teachyou.ai academy
← All posts
n8n

n8n Performance Tuning: Speeding Up Slow Workflows

Pramod Dutta · Jun 6, 2026 · 16 min read

Why Your n8n Workflow Feels Sluggish

You built a workflow that looked clean in the editor. Ten nodes, a couple of branches, maybe an AI Agent node calling a model somewhere in the middle. It worked fine with test data. Then it went into production, started processing real volumes, and now every execution takes twelve seconds instead of two. Or worse, executions start timing out, the queue backs up, and you're staring at a dashboard full of "waiting" statuses wondering what changed.

Nothing changed, really. n8n workflows don't get slow because you did something obviously wrong. They get slow because of a handful of specific, well-understood bottlenecks that show up predictably as data volume, node count, and concurrency increase. The good news is that almost all of them are fixable without rewriting your automation from scratch. The bad news is that the default settings in a fresh n8n instance are tuned for getting started quickly, not for running efficiently at scale, so if you never touch the configuration, you will eventually hit a wall.

This article walks through where n8n performance actually goes wrong — database bloat from execution logging, inefficient expressions and code nodes, HTTP request patterns, database and API bottlenecks inside workflows, and the difference between running in regular mode versus queue mode. Each section includes concrete settings, code, and a way to verify the fix actually helped. None of this is theoretical. These are the same categories of problems that show up over and over in production n8n deployments once you move past a handful of simple workflows.

Start By Measuring, Not Guessing

Before changing anything, figure out where the time is actually going. n8n gives you this for free if you know where to look.

Open any execution in the editor and look at the timing displayed on each node after a run. n8n shows execution time per node directly in the canvas — hover over a node after execution and you'll see how long it took. For a slow workflow, run it once and look at which node ate the most time. Nine times out of ten, it's not the node you suspect.

Common culprits, roughly in order of how often they turn out to be the real problem:

  • An HTTP Request node calling a slow or rate-limited third-party API
  • A Code node doing something O(n²) over an array that grew larger than expected
  • A Postgres/MySQL node running a query without an index
  • A Split In Batches setup that's serializing work that could run in parallel
  • The workflow execution log itself, once your instance has run for months without cleanup

That last one surprises people, so it's worth its own section, because it's usually the first thing to check when a previously-fast instance slows down globally rather than for one workflow.

Execution Data Is Probably Your Biggest Hidden Cost

Every time a workflow runs, n8n writes an execution record to its database — by default including the full input and output data for every node. If you're running hundreds or thousands of executions a day, this table grows fast, and a bloated executions table slows down everything: the editor takes longer to load, the executions list becomes sluggish, and even unrelated workflow triggers can lag because the database is busy.

Check your environment variables. If you haven't set data pruning, do it now:

EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=336
EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000

EXECUTIONS_DATA_MAX_AGE is in hours, so 336 is two weeks. Adjust based on how long you actually need execution history for debugging or compliance. There's no reason to keep a year of full execution payloads sitting in Postgres if you only ever look back a few days.

You can also control this more granularly per workflow using the "Save Data Success Execution" and "Save Data Error Execution" settings in workflow settings, or globally with:

EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
EXECUTIONS_DATA_SAVE_ON_ERROR=all
EXECUTIONS_DATA_SAVE_ON_PROGRESS=false

Setting success executions to none means you stop saving full data for workflows that complete without error — you'll still get a record that it ran, but not the entire payload of every node. This alone can cut database size dramatically for high-volume, low-failure-rate workflows like webhook ingestion pipelines. Keep error executions fully logged, because that's exactly when you need the data to debug.

If your executions table is already huge, don't just flip these settings and walk away — you still need to clean up the historical bloat:

DELETE FROM execution_entity
WHERE "finished" = true
AND "stoppedAt" < NOW() - INTERVAL '30 days';

Run this in a maintenance window, back up first, and check whether n8n's built-in pruning job is already scheduled to handle this incrementally going forward before you resort to manual SQL.

Expressions and the Code Node: Where CPU Time Actually Goes

n8n expressions are convenient, but convenience has a cost when they're evaluated repeatedly across large item arrays. A few patterns consistently cause slowdowns:

Re-fetching or re-computing the same value inside a loop. If you have an expression like {{ $json.items.filter(i => i.active) }} referenced in multiple places across a workflow, n8n recalculates it every time it's referenced rather than caching the result. If you need the same derived value in three different nodes, compute it once in a Set/Edit Fields node or a Code node and reference the stored field afterward.

Doing heavy lifting inside `$json` expressions instead of a Code node. Expressions are meant for lightweight transforms — string interpolation, simple math, basic array access. If you're chaining .map(), .filter(), and .reduce() together inside a single expression field, move that logic into a Code node where it runs once as actual JavaScript rather than being re-parsed by the expression engine on every evaluation.

Code nodes running in "Run Once for Each Item" mode when they don't need to. This is one of the most common accidental performance killers. If your Code node's logic doesn't actually depend on per-item state, switch it to "Run Once for All Items." The difference is significant: for a batch of 5,000 items, "Run Once for Each Item" means n8n spins up the JS execution context 5,000 times. "Run Once for All Items" runs it once and lets you iterate internally, which is dramatically faster.

// Slow pattern: Run Once for Each Item
// This re-executes for every single item in the input
const enriched = {
  ...$json,
  processedAt: new Date().toISOString(),
  score: $json.value * 1.15
};
return enriched;
// Fast pattern: Run Once for All Items
// Same logic, executed once over the whole array
const items = $input.all();
return items.map(item => ({
  json: {
    ...item.json,
    processedAt: new Date().toISOString(),
    score: item.json.value * 1.15
  }
}));

For genuinely large datasets — tens of thousands of items or more — avoid building intermediate arrays with .map().filter().map() chains that each allocate a new array. A single for loop or a reduce that does the work in one pass avoids the extra allocations and garbage collection pressure.

// Multiple passes, three allocations
const result = items
  .map(i => transform(i))
  .filter(i => i.valid)
  .map(i => finalize(i));

// Single pass, one allocation
const result = [];
for (const item of items) {
  const t = transform(item);
  if (t.valid) result.push(finalize(t));
}

This kind of micro-optimization doesn't matter for 50 items. It matters a great deal for 50,000.

HTTP Request Nodes: Batching, Pagination, and Rate Limits

The HTTP Request node is usually the single slowest node in any workflow, because it's bound by network latency and a third party's response time, not by n8n itself. But there are still real gains to be made here.

Enable pagination properly instead of looping manually. If you're using a Loop node wrapped around an HTTP Request node to page through an API, check whether the HTTP Request node's built-in pagination feature can replace it. Native pagination avoids the overhead of re-entering the node repeatedly through the loop mechanism and generally produces cleaner, faster execution.

Batch your requests where the API allows it. If you're calling an endpoint once per record when the API supports bulk operations (send 100 IDs in one call instead of 100 calls with one ID each), you're multiplying your latency unnecessarily. Check the API docs for batch or bulk endpoints before assuming per-item calls are the only option.

Use Split In Batches deliberately, with a sane batch size. When you must call an API per item, don't fire all 10,000 requests concurrently — you'll get rate-limited, and n8n will spend time retrying failed calls, which is slower overall than a controlled, paced batch. A batch size of 10-50 with a small delay between batches is usually far more reliable than an unbounded burst.

// Inside a Code node, controlled concurrency for outbound calls
const batchSize = 20;
const delayMs = 200;
const results = [];

for (let i = 0; i < items.length; i += batchSize) {
  const batch = items.slice(i, i + batchSize);
  const batchResults = await Promise.all(
    batch.map(item => callExternalApi(item.json))
  );
  results.push(...batchResults);
  if (i + batchSize < items.length) {
    await new Promise(resolve => setTimeout(resolve, delayMs));
  }
}

Set explicit timeouts. A hanging HTTP Request node with no timeout configured can stall an entire execution waiting on a server that's never going to respond. Set a reasonable timeout (a few seconds to maybe 30, depending on the API) rather than relying on defaults, so a single bad call fails fast instead of hanging the whole run.

Database Nodes: Indexes and Query Shape Matter More Than n8n Settings

If your workflow reads from or writes to Postgres, MySQL, or another database, the slowness often has nothing to do with n8n at all — it's the query.

A Postgres node running SELECT * FROM orders WHERE customer_email = $1 against a table with no index on customer_email will do a sequential scan every single time, and that cost scales linearly with table size. As the table grows from 10,000 to 10 million rows, that query goes from instant to genuinely slow, and n8n gets blamed for a problem that lives entirely in the database schema.

Check execution plans for any query your workflow runs frequently:

EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_email = 'test@example.com';

If you see Seq Scan where you'd expect an index lookup, add the index:

CREATE INDEX idx_orders_customer_email ON orders(customer_email);

Also watch for workflows that run one query per item instead of a single batched query. If you're iterating over 500 items and running an individual INSERT or UPDATE for each one, switch to a single batched statement wherever the node supports it, or construct a multi-row insert in a Code node and execute it as one query. A thousand round trips to the database will always be slower than one round trip with a thousand rows.

Regular Mode vs Queue Mode: The Architecture Question

If you're running n8n in the default "regular" mode — a single process handling both the UI and workflow execution — you will eventually hit a ceiling no amount of node-level tuning can fix. Every workflow execution competes with every other execution and with the editor itself for the same process resources. Under load, this shows up as executions queuing up internally, webhooks timing out before the workflow even starts, and the editor becoming unresponsive while workflows are running.

Queue mode solves this by separating concerns: a main process handles the UI and webhook reception, while one or more separate worker processes actually execute the workflows, coordinated through Redis.

EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=your-redis-host
QUEUE_BULL_REDIS_PORT=6379

Then run dedicated worker processes:

n8n worker --concurrency=10

The --concurrency flag controls how many executions a single worker process handles simultaneously. This is the actual lever for throughput — if you're seeing executions pile up in the queue faster than they're being processed, you either need higher concurrency per worker or more worker processes, not a faster CPU on the main instance.

A few things worth knowing before you flip this switch:

  • Queue mode requires Redis, so that becomes a new piece of infrastructure you're responsible for keeping healthy.
  • Webhook-triggered workflows benefit the most, since the main process can accept the webhook and hand it off instantly rather than blocking on execution.
  • You can scale workers horizontally — run multiple worker containers/processes, each pulling from the same queue — which is the standard way to handle unpredictable traffic spikes without over-provisioning a single machine.
  • Don't set concurrency arbitrarily high without checking what the workflows actually do. A worker running ten concurrent executions that each open a database connection can exhaust your connection pool before it exhausts CPU.

If you're self-hosting and still on regular mode with meaningful production traffic, this is usually the single highest-leverage change available — bigger than any individual node optimization, because it changes the architecture rather than patching a symptom.

Trigger and Webhook Design: Don't Make the Trigger Do Too Much

A pattern worth calling out specifically: workflows where the trigger node itself, or the first few nodes after it, do more work than they should before handing off to the real logic.

If a webhook-triggered workflow does synchronous processing before responding to the caller, the caller waits for the entire workflow to finish, not just the initial handoff. For any workflow where the caller doesn't need the result immediately, set the Webhook node to respond immediately ("Respond Immediately" or via a separate "Respond to Webhook" node placed early) and let the rest of the workflow continue asynchronously. This matters enormously for integrations with services that have their own timeout expectations — many third-party webhook senders will consider the call failed and retry it if you take too long to return a 200, which can double or triple your effective load through duplicate deliveries.

Similarly, for Schedule Triggers that kick off large batch jobs, check whether the workflow is doing one enormous run versus several smaller scheduled runs. A single 2 AM job processing 100,000 records in one execution is harder to monitor, harder to retry piecewise if it fails partway through, and holds resources for a long continuous stretch. Splitting it into several scheduled runs, or paginating internally with checkpoints, often produces both better performance and better fault tolerance.

AI Agent Nodes and LLM Calls: A Special Case

If your slow workflow includes an AI Agent node or direct LLM API calls, the calculus is a bit different from typical HTTP requests, because you're paying in both latency and token cost, and the two interact.

Trim the context you send. Every extra token in a prompt is time spent both transmitting and processing. If you're passing entire conversation histories, full documents, or bulky tool outputs into an Agent node's context on every call, look for ways to summarize or truncate before the model call rather than after. Passing only what's relevant to the current step is faster and cheaper.

Watch tool-calling loops. An AI Agent node configured with several tools can end up making multiple sequential round trips to the model as it reasons about which tool to call, calls it, evaluates the result, and decides whether to call another. This is inherent to how agentic loops work, but it means an Agent node is rarely going to be your fastest node, and chaining multiple Agent nodes in sequence multiplies that latency directly. Where possible, consolidate tool calls or use a single well-scoped Agent step rather than several smaller ones stacked in a row.

Cache identical or near-identical calls. If your workflow calls an LLM with the same or highly similar prompt repeatedly — for example, classifying the same category of input over and over — consider a simple caching layer (a database lookup keyed on a hash of the input) before making the call at all. This is a case where a five-line check against a Postgres table can eliminate the slowest, most expensive node in the entire workflow for a meaningful fraction of runs.

Pick the right model for the step. Not every node in an agentic pipeline needs your most capable and slowest model. Classification, extraction, and formatting steps often work fine with a smaller, faster model, reserving the heavier model for the step that actually needs deep reasoning. This is a performance lever as much as a cost lever, and it's one of the most underused optimizations in n8n workflows that lean on AI Agent nodes throughout.

A Practical Checklist Before You Call It "Optimized"

Pulling this together, here's a reasonable order of operations for tuning a slow n8n workflow or instance:

  1. Check per-node execution times in the editor to identify the actual bottleneck rather than guessing
  2. Set execution data pruning and disable full-data saving on successful runs if you don't need it
  3. Audit Code nodes for "Run Once for Each Item" where "Run Once for All Items" would work
  4. Replace manual pagination loops with native HTTP Request pagination where available
  5. Add missing indexes for any database query running inside a workflow
  6. Batch outbound API calls and database writes instead of looping one at a time
  7. Set explicit timeouts on HTTP Request nodes
  8. Move to queue mode with Redis if you're running meaningful production volume on regular mode
  9. Make webhook-triggered workflows respond immediately when the caller doesn't need a synchronous result
  10. For AI-heavy workflows, trim context, cache repeat calls, and right-size the model per step

None of these require rebuilding your automations. Most are configuration changes or targeted rewrites of one or two nodes. The instances that stay fast as they scale are almost always the ones where someone went through a list like this once volume started climbing, rather than waiting until the workflow was falling over in production.

Closing Thoughts

Performance problems in n8n rarely come from n8n itself being slow — they come from patterns that work fine at small scale and quietly break down as data volume, execution frequency, or workflow complexity grows. The fixes are mechanical once you know where to look: prune your execution data, fix your Code node execution mode, batch your API and database calls, and move to queue mode when regular mode stops keeping up. Treat performance tuning as a normal part of maintaining a workflow, not a one-time fire drill, and you'll catch these issues while they're still cheap to fix.

If you want to go deeper into building production-grade automations with n8n — including AI Agent orchestration, tool design, and the exact patterns covered here applied to real agentic workflows — check out the n8n AI Agent Tutorial course on teachyou.ai. It walks through building, debugging, and scaling agentic n8n workflows from the ground up, with the same practical, no-fluff approach as this article.