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.

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_ENABLEDis deprecated. N8N_RUNNERS_MODEdefaults tointernal. 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=externaland a sharedN8N_RUNNERS_AUTH_TOKENon n8n, and run ann8nio/runnerscontainer 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_TIMEOUTbefore 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
5679by 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
| Question | Internal mode | External mode |
|---|---|---|
| Who starts the runner | n8n, as a child process | A launcher inside the n8nio/runners container |
| Runs as | The same user (uid/gid) and host as n8n | A separate container |
| Default | Yes (N8N_RUNNERS_MODE=internal) | No, you opt in |
| Python Code node | Only 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 |
| Status | Deprecated, "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/runnersimage version must be the same as then8nio/n8nversion. - 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)" >> .envWhat each setting does:
N8N_RUNNERS_MODE=externaltells n8n not to start its own runner and to wait for an external one.N8N_RUNNERS_AUTH_TOKENis the shared secret the runner uses to connect. It must be identical on both containers. If it's missing in external mode, n8n throws aMissing auth tokenerror when it starts its task runner module, saying the token is required whenN8N_RUNNERS_MODEisexternal.N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0is needed because the broker listens only on127.0.0.1by default, which another container can't reach.N8N_RUNNERS_TASK_BROKER_URIpoints the runner at the broker: the n8n service name and port5679.
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 likeitem.json.my_field. - Insecure built-ins such as
eval,execandopenare 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 examplecrypto.NODE_FUNCTION_ALLOW_EXTERNAL: third-party JavaScript packages, for examplemoment,uuid.N8N_RUNNERS_STDLIB_ALLOW: Python standard library modules, for examplejson.N8N_RUNNERS_EXTERNAL_ALLOW: third-party Python packages, for examplenumpy,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 runnerIn 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.jsonTwo 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:
| Variable | Default in 2.44.7 | What it controls |
|---|---|---|
N8N_RUNNERS_MODE | internal | Internal or external mode |
N8N_RUNNERS_AUTH_TOKEN | not set | Shared secret between n8n and the runners; required in external mode |
N8N_RUNNERS_BROKER_LISTEN_ADDRESS | 127.0.0.1 | Address the broker binds to |
N8N_RUNNERS_BROKER_PORT | 5679 | Port the broker listens on |
N8N_RUNNERS_TASK_TIMEOUT | 300 seconds | How long a task may run before it's stopped; 60 from n8n 3.0 |
N8N_RUNNERS_TASK_REQUEST_TIMEOUT | 60 seconds | How long a task waits for a free runner before it fails |
N8N_RUNNERS_HEARTBEAT_INTERVAL | 30 seconds | How often a runner must report in; a missed heartbeat aborts the task |
N8N_RUNNERS_MAX_CONCURRENCY | 10 | Tasks one runner executes at the same time |
N8N_RUNNERS_MAX_OLD_SPACE_SIZE | empty (Node.js decides) | --max-old-space-size (memory limit in MB) of the runner |
N8N_RUNNERS_INSECURE_MODE | false | Turns 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:
- Shorter default timeout.
N8N_RUNNERS_TASK_TIMEOUTdrops from300to60seconds, 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 to60." $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 runningN8N_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.- Manual executions always run on workers in queue mode.
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERSis 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
-distrolessto the image tag); - running the runners as the unprivileged
nobodyuser (user and group ID65532); - a read-only root filesystem with a small writable volume on
/tmp; - an AppArmor rule that blocks reading
/proc/*/environand/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
- Set up task runners (n8n docs)
- Task runner environment variables (n8n docs)
- Hardening task runners (n8n docs)
- n8n v2.0 breaking changes (n8n docs)
- n8n 3.0 breaking changes (n8n docs)
- Code node, Python (Native) (n8n docs)
- Task runner config in n8n 2.44.7 (n8n source on GitHub)
- Default launcher config of the n8nio/runners image, n8n 2.44.7 (n8n source on GitHub)
- Deprecation warnings in n8n 2.44.7 (n8n source on GitHub)
- Task runner error messages in n8n 2.44.7 (n8n source on GitHub)
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.



