Skip to content
Guides

n8n task runners explained: internal vs external mode and Python

What n8n task runners do, why internal mode is deprecated, how to run them in external mode for the Code node and Python, and what changes in n8n 3.0.

By then8nblog Editors10 min read
intermediate
Illustration of n8n task runners: internal mode is deprecated but still the default in 2.x, external mode runs an n8nio/runners sidecar of the same version, native Python needs external mode, and n8n 3.0 lowers the task timeout to 60 seconds

Task runners are the separate processes that run the JavaScript and Python you write in the Code node, so that your code doesn't run inside n8n itself. They run in one of two modes: internal, where n8n starts the runner as a child process on the same host and user, and external, where an n8nio/runners sidecar container runs them. Internal mode is still the default, but n8n has deprecated it, and n8n's docs say to use external mode in production because it's the only real isolation between Code node users and your credentials.

The short version:

  • Since n8n 2.0, every Code node execution runs on a task runner. You don't enable them anymore; N8N_RUNNERS_ENABLED is deprecated.
  • N8N_RUNNERS_MODE defaults to internal. In n8n 2.44.7, n8n already logs a deprecation warning at startup when the mode is internal, including when you never set it.
  • External mode means: set N8N_RUNNERS_MODE=external and a shared N8N_RUNNERS_AUTH_TOKEN on n8n, and run an n8nio/runners container with the same version as your n8n image.
  • Native Python in the Code node needs external mode. The old Pyodide-based Python is gone since 2.0.
  • n8n 3.0 lowers the default task timeout from 300 to 60 seconds. If your Code nodes run longer, set N8N_RUNNERS_TASK_TIMEOUT before you upgrade.

This guide is based on n8n's documentation and the n8n 2.44.7 source code (the current stable release on npm as of October 27, 2026). We didn't run the configuration below in an n8n instance for this post. For a complete setup that was tested end to end, see our Docker Compose guide, which includes an external runner.

What task runners are and how they work

n8n's task runner docs describe three parts:

  • Task requester: the Code node, which submits the code to run.
  • Task broker: the n8n instance itself (main or worker). It listens for runners on port 5679 by default.
  • Task runner: a process that connects to the broker over a WebSocket, picks up a task, runs it and sends the result back.

There is one runner per language: a JavaScript runner and a Python runner. Each runner can execute several tasks at once (N8N_RUNNERS_MAX_CONCURRENCY).

The reason this matters is security. In n8n's own words, task runners "are the only isolation layer between user-provided code and n8n. Without them, or with internal mode, anyone who can edit a workflow could potentially read your database, encryption key, stored credentials, and environment variables."

Internal vs external mode

QuestionInternal modeExternal mode
Who starts the runnern8n, as a child processA launcher inside the n8nio/runners container
Runs asThe same user (uid/gid) and host as n8nA separate container
DefaultYes (N8N_RUNNERS_MODE=internal)No, you opt in
Python Code nodeOnly if Python 3 and the runner's virtual environment exist on the host (not the case in the standard n8n Docker image)Yes, the runners image ships both runners
StatusDeprecated, "will be removed in a future version"Recommended by n8n for production

Internal mode

In internal mode, n8n launches the runner itself and manages its life cycle. n8n's docs call it "insecure by design": the runner runs as the same user on the same host as n8n, so code that escapes the runner's sandbox has the same access as n8n. The docs say to use it only on isolated instances that hold no sensitive data.

Internal mode is what you get if you never set N8N_RUNNERS_MODE. The docs list the deprecation under n8n 3.0, but the deprecation check in the 2.44.7 source already warns at startup whenever the mode resolves to internal:

Internal task runner mode is deprecated and will be removed in a future version.

If you see that line in your logs, it applies to you even though you didn't configure anything.

External mode

In external mode, a launcher starts the runners on demand and restarts them when needed. You run it as a sidecar container from the n8nio/runners image, which contains the launcher, the JavaScript runner and the Python runner. Since n8n 2.0, the main n8nio/n8n image no longer includes the runner for external mode, so you need the separate image (2.0 breaking changes).

Two rules from the docs that people miss:

  • Versions must match. The n8nio/runners image version must be the same as the n8nio/n8n version.
  • Every process that runs Code nodes needs its own sidecar. In queue mode, each worker gets its own runners container. On n8n 2.x, if you set OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=false, the main instance runs manual executions and needs one too.

Set up external mode with Docker Compose

This is the shape of n8n's reference setup from the task runner docs, with the version moved into a variable so both images stay in sync:

services:
  n8n:
    image: n8nio/n8n:${N8N_VERSION}
    container_name: n8n-main
    environment:
      - N8N_RUNNERS_MODE=external
      - N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
      - N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
      - N8N_NATIVE_PYTHON_RUNNER=true
    ports:
      - "5678:5678"
    volumes:
      - n8n_data:/home/node/.n8n

  task-runners:
    image: n8nio/runners:${N8N_VERSION}   # must match the n8n image
    container_name: n8n-runners
    environment:
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-main:5679
      - N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
    depends_on:
      - n8n

volumes:
  n8n_data:

Put both values in an .env file next to the Compose file, and generate the token instead of typing one:

echo "N8N_VERSION=<your-n8n-version>" >> .env
echo "RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)" >> .env

What each setting does:

  • N8N_RUNNERS_MODE=external tells n8n not to start its own runner and to wait for an external one.
  • N8N_RUNNERS_AUTH_TOKEN is the shared secret the runner uses to connect. It must be identical on both containers. If it's missing in external mode, n8n throws a Missing auth token error when it starts its task runner module, saying the token is required when N8N_RUNNERS_MODE is external.
  • N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0 is needed because the broker listens only on 127.0.0.1 by default, which another container can't reach.
  • N8N_RUNNERS_TASK_BROKER_URI points the runner at the broker: the n8n service name and port 5679.

The runner image can't read secrets from files: variables with a _FILE suffix don't work there, according to the environment variable reference. Pass the token as a normal environment variable.

Python in the Code node

Python in the Code node now runs natively on the Python task runner. n8n 2.0 removed the Pyodide-based version and says that from 2.0 you can only use Python Code nodes with task runners in external mode.

In internal mode on 2.44.7, n8n only starts a Python runner if it finds Python 3 and the runner's virtual environment on the host. Otherwise n8n still starts, logs a warning, and Python Code nodes fail when they run. The warning text is:

Failed to start Python task runner in internal mode. because Python 3 is missing from this system.

The same message ends with a hint that internal mode for Python "is intended only for debugging" and that you should deploy in external mode.

If you're porting Python from the old Pyodide node, the Code node docs list the differences:

  • Only _items (in Run Once for All Items mode) and _item (in Run Once for Each Item mode) are available. Other n8n built-in methods and variables aren't.
  • Use bracket access, item["json"]["my_field"], not dot access like item.json.my_field.
  • Insecure built-ins such as eval, exec and open are denied by default (N8N_RUNNERS_BUILTINS_DENY).
  • On n8n Cloud you can't import any Python modules. On self-hosted n8n you can, if the runners image includes them and you allowlist them.

Allow modules and add packages

By default, the Code node can't import much. The n8nio/runners image's default launcher config in 2.44.7 allows the Node.js crypto module and the moment package for JavaScript, and no modules at all for Python.

In external mode, you change these allowlists in the launcher config file, not as container environment variables; the docs repeat this for each variable. The variables are:

  • NODE_FUNCTION_ALLOW_BUILTIN: Node.js built-in modules for JavaScript, for example crypto.
  • NODE_FUNCTION_ALLOW_EXTERNAL: third-party JavaScript packages, for example moment,uuid.
  • N8N_RUNNERS_STDLIB_ALLOW: Python standard library modules, for example json.
  • N8N_RUNNERS_EXTERNAL_ALLOW: third-party Python packages, for example numpy,pandas.

To add packages, n8n's docs suggest extending the runners image (you need at least n8nio/runners:1.121.0 for this), installing the packages, and copying in your own config:

FROM n8nio/runners:<your-n8n-version>
USER root
RUN cd /opt/runners/task-runner-javascript && pnpm add moment uuid
RUN cd /opt/runners/task-runner-python && uv pip install numpy pandas
COPY n8n-task-runners.json /etc/n8n-task-runners.json
USER runner

In your copy of n8n-task-runners.json, set the allowlists in each runner's env-overrides. Start from the default file of your exact version rather than writing one from scratch, because it also holds the commands, arguments and health check ports of both runners. If you only want to change the config, mount your file instead of building an image:

    volumes:
      - ./n8n-task-runners.json:/etc/n8n-task-runners.json

Two more Python settings are worth knowing: N8N_RUNNERS_ALLOW_TRANSITIVE_IMPORTS=true lets an allowlisted package load its own dependencies without listing each one (useful for pandas), and N8N_BLOCK_RUNNER_ENV_ACCESS (default true) keeps os.environ out of reach of Python code. Both trade security for convenience, so change them only on purpose.

Environment variables worth knowing

These are the n8n-side settings from the reference, with the defaults we found in the 2.44.7 source:

VariableDefault in 2.44.7What it controls
N8N_RUNNERS_MODEinternalInternal or external mode
N8N_RUNNERS_AUTH_TOKENnot setShared secret between n8n and the runners; required in external mode
N8N_RUNNERS_BROKER_LISTEN_ADDRESS127.0.0.1Address the broker binds to
N8N_RUNNERS_BROKER_PORT5679Port the broker listens on
N8N_RUNNERS_TASK_TIMEOUT300 secondsHow long a task may run before it's stopped; 60 from n8n 3.0
N8N_RUNNERS_TASK_REQUEST_TIMEOUT60 secondsHow long a task waits for a free runner before it fails
N8N_RUNNERS_HEARTBEAT_INTERVAL30 secondsHow often a runner must report in; a missed heartbeat aborts the task
N8N_RUNNERS_MAX_CONCURRENCY10Tasks one runner executes at the same time
N8N_RUNNERS_MAX_OLD_SPACE_SIZEempty (Node.js decides)--max-old-space-size (memory limit in MB) of the runner
N8N_RUNNERS_INSECURE_MODEfalseTurns off the runner's security measures; n8n discourages it in production

One discrepancy: the docs table lists 5 as the default for N8N_RUNNERS_MAX_CONCURRENCY, while the 2.44.7 source sets 10, both in n8n's config and in the runner's own config. If the number matters to you, set it explicitly.

On the runner side, N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT (default 15 seconds) stops an idle runner; the launcher starts it again when a new task arrives. Set it to 0 to keep runners running.

What changes for task runners in n8n 3.0

n8n's 3.0 breaking changes page has three items that touch task runners. The 3.0 beta was announced for October 27, 2026, but it wasn't on npm when we checked that morning, so these are documented changes, not tested ones:

  1. Shorter default timeout. N8N_RUNNERS_TASK_TIMEOUT drops from 300 to 60 seconds, and Code node tasks that run longer fail. If you have long-running Code nodes, set the variable explicitly now. The 2.44.7 source comment says the same: "n8n v3 will reduce this to 60."
  2. $evaluateExpression() is removed from the Code node. It already fails on runners in secure mode, the default since 2.0, so this only affects instances running N8N_RUNNERS_INSECURE_MODE=true. Evaluate the expression in a field of an earlier node, such as Edit Fields (Set), and read the result in the Code node.
  3. Manual executions always run on workers in queue mode. OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS is removed, so the case above where the main instance runs manual executions goes away, and your workers handle them instead.

Internal mode itself isn't removed in 3.0: the docs say it's deprecated and "will be removed in a future version." Moving to external mode before you upgrade is still the cleaner order, since you change one thing at a time. Our n8n 3.0 guide covers the rest of the release, and the one-way upgrade guide covers the backup you need before it.

Common errors and fixes

These messages come from the task runner error classes in the 2.44.7 source.

Task request timed out

The full message says the Code node task "was not matched to a runner within the timeout period" and that the runner "is currently down, or not ready, or at capacity." In external mode, check in this order: the runners container is running, it uses the same version as n8n, N8N_RUNNERS_AUTH_TOKEN is identical on both sides, N8N_RUNNERS_TASK_BROKER_URI points at the right service name and port, and n8n has N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0. If the runner is healthy but busy, raise N8N_RUNNERS_MAX_CONCURRENCY or N8N_RUNNERS_TASK_REQUEST_TIMEOUT.

Task execution aborted because runner became unresponsive

The runner missed its heartbeat. n8n's suggestions: break CPU-heavy work into smaller batches, make sure every path in the script can finish (no infinite loops), and on self-hosted n8n raise N8N_RUNNERS_HEARTBEAT_INTERVAL if a task can legitimately keep the runner busy for longer than 30 seconds.

Node ran out of memory

Too many items in one Code node run. Process items in batches with a loop node (see Loop Over Items), or raise the runner's memory with N8N_RUNNERS_MAX_OLD_SPACE_SIZE (in MB) on self-hosted n8n.

A module import is blocked

The module isn't in the allowlist, or isn't installed in the runners image. In external mode, set the allowlist in /etc/n8n-task-runners.json as described above; setting NODE_FUNCTION_ALLOW_EXTERNAL or N8N_RUNNERS_EXTERNAL_ALLOW on the container doesn't reach the runner.

Harden the runners

If the instance holds real credentials, n8n's hardening guide recommends, on top of external mode:

  • the distroless image variant (add -distroless to the image tag);
  • running the runners as the unprivileged nobody user (user and group ID 65532);
  • a read-only root filesystem with a small writable volume on /tmp;
  • an AppArmor rule that blocks reading /proc/*/environ and /proc/*/mounts, which can expose secrets to code inside the container.

For the full production picture (Postgres, HTTPS, backups and an external runner in one Compose file), see Self-host n8n with Docker Compose, Postgres and HTTPS, and when you change versions, follow How to update self-hosted n8n safely so the n8n and runners images move together.

Sources

  1. Set up task runners (n8n docs)
  2. Task runner environment variables (n8n docs)
  3. Hardening task runners (n8n docs)
  4. n8n v2.0 breaking changes (n8n docs)
  5. n8n 3.0 breaking changes (n8n docs)
  6. Code node, Python (Native) (n8n docs)
  7. Task runner config in n8n 2.44.7 (n8n source on GitHub)
  8. Default launcher config of the n8nio/runners image, n8n 2.44.7 (n8n source on GitHub)
  9. Deprecation warnings in n8n 2.44.7 (n8n source on GitHub)
  10. Task runner error messages in n8n 2.44.7 (n8n source on GitHub)

then8nblog Editors

Editorial team

The then8nblog editorial team researches every article from n8n's release notes, documentation and source code, states the n8n version each guide was tested on, and has every post reviewed by a human editor before it goes live.