JavaScript Code Node Fails After 60s with High CPU Load – Python Works Fine

Hi n8n community,

I’m running n8n in both Enterprise (Kubernetes/Linux) and Community (Docker Desktop/Windows 11) editions, with Queue Mode enabled (2 workers) and external Task Runners for JavaScript and Python execution.

During load testing, I created two Code Nodes:

  • JavaScript: Generates high CPU load for a defined duration (e.g., 70s).
  • Python: Does the same.

Observation:

  • JavaScript: After ~60s, the runner is terminated with:

    2026/09/14 14:00:20 WARN [launcher:js] Found runner unresponsive (n/6)

    …

    2026/09/14 14:00:40 WARN [launcher:js] Found runner unresponsive too many times, terminating runner…

    The workflow fails with an error: Task execution aborted because runner became unresponsive.

  • Python: Runs smoothly for the full 70s, with health checks passing every 10s:

    2026/09/16 07:55:20 DEBUG [launcher:py] Found runner healthy

Configuration:

  • N8N_RUNNERS_TASK_TIMEOUT=300 (5-minute task limit)
  • N8N_RUNNERS_TASK_REQUEST_TIMEOUT=300 (5-minute start timeout)
  • N8N_RUNNERS_HEARTBEAT_INTERVAL=120 (2-minute heartbeat, no effect)

Hypothesis:
The JavaScript runner’s Node.js process is blocked by synchronous, CPU-intensive operations (e.g., tight loops), preventing it from responding to health checks. Python, running in a separate runtime, doesn’t block Node.js, so health checks succeed.

Question:
Is there a way to run high-CPU JavaScript code for >60s without the runner being terminated? For example:

  • A way to respond to health checks despite a high CPU load with JS?
  • A configuration tweak to extend the “unresponsive” tolerance?
  • Or is this a fundamental limitation of the JS runner’s design?

Thanks for any insights!

@alatten

The issue stems from the single-threaded nature of the Node.js Event Loop used by the JavaScript task runner. When you execute heavy, synchronous, or CPU-intensive JavaScript (such as a tight for loop), you effectively block the Event Loop from processing any other tasks. Since the runner relies on this same loop to respond to n8n’s health checks and heartbeat signals, the process becomes incapable of acknowledging the launcher’s probes, leading to the “unresponsive” error and subsequent termination.

In contrast, the Python runner is more resilient because it operates as a separate process. While the Python interpreter may be consuming 100% of the CPU, the architectural separation allows the communication layer between the n8n launcher and the runner to remain functional. Because the heavy computation does not block the management signals in the same way it does within the Node.js environment, the Python runner can continue to pass health checks even under extreme load.

To prevent this in JavaScript, you must avoid long-running synchronous blocks by implementing “cooperative multitasking.” This can be achieved by breaking large tasks into smaller chunks and using await new Promise(resolve => setImmediate(resolve)) to periodically yield control back to the Event Loop, allowing health checks to process. Alternatively, for purely computational workloads, it is best practice to offload the logic to the Python runner or an external service where high CPU usage won’t trigger a runner termination.

Hi @alatten
The missing detail is that your log comes from the launcher’s HTTP health checks. N8N_RUNNERS_HEARTBEAT_INTERVAL controls the runner-to-broker heartbeat, so increasing it doesn’t extend this separate check.

In launcher 1.4.7, the probe interval is hardcoded to 10 seconds, the response timeout to 5 seconds, and termination happens after six consecutive failures. There isn’t an environment variable for that threshold in this version.

Thanks for letting us know about this, We have created CAT-4556 as the internal dev ticket to look into it.

Recommended solution is to Yield the event loop

Break the heavy work into small chunks and periodically give control back:

const start = Date.now();
const duration = 70_000; // 70 seconds

while (Date.now() - start < duration) {
// Do a chunk of heavy work here
for (let i = 0; i < 100000; i++) {
// your CPU work
}

// Yield so health checks can run
await new Promise(resolve => setImmediate(resolve));
}

You can also use await new Promise(r => setTimeout(r, 0)).