Module 6: Scaling And Performance Queue Mode

2. Where the time actually goes

Description

By the end of this lesson you'll be able to open an execution in n8n and read, node by node, how long each one took, so you know with evidence where a workflow's time goes before touching a single checkbox. You'll be able to distinguish the three kinds of time —compute, network wait, and queue— because each is fixed in a different way, and —most importantly— you'll learn not to fall into the trap that fools almost everyone: the node that looks like the slowest often isn't the culprit, it's just the one waiting on the culprit.

This matters because it's the foundation of the whole module, and without it the rest is a game of chance. Optimizing without measuring is guessing, and guessing about performance is expensive twice over: it costs the hours you invest polishing the wrong node, and it costs the real problem that's still there because you never looked at it. Measuring first isn't the boring part before the interesting work. Measuring is the interesting work: it's what turns "the workflow is slow" —a complaint— into "the node that calls the erp takes 95% of the time" —a plan—.

Connection to the module: lesson 1 gave you the thesis (fix the workflow before buying a machine) and the three buckets the time splits into. This lesson gives you the instrument to see those buckets: how to read a measured execution. It's the lesson that enables all the others —3, 4, 5, and 6 are optimization techniques, and which of them you apply depends entirely on what this measurement tells you—. A clear boundary: Module 4 of this guide, the observability one, measures similar things —duration, percentiles, metrics over time— but from the angle of what you monitor in general to operate. Here we measure with a single, much narrower purpose: finding a specific workflow's bottleneck in order to optimize it. We won't set up dashboards; we'll read an execution.

An itemized receipt, not the bill total

Think about the difference between two ways of getting the bill at a restaurant.

The first is a ticket that says, and nothing more: "Total: 1,180 pesos." With that you know you spent a lot, but you don't know on what. Was it the wine? The desserts? Did someone order a super-expensive dish you didn't notice? You can't do anything useful with that number, except complain that the bill came out expensive.

The second is an itemized receipt, line by line: the appetizer 90, the three main courses 640, the wine 320, the desserts 130. With that receipt, in five seconds you see that the wine and the mains took almost everything, and if you wanted to spend less next time, you know exactly where to cut. The total is identical on both receipts; what changes is that one lets you act and the other only lets you feel.

When someone says "my workflow takes 11 minutes," they're reading the first ticket: the total. Slowness is a feeling, not a plan. What you need is the second receipt —how long each node took— because slowness is almost never spread evenly. It's concentrated, like the wine on the bill, in one or two lines that take almost everything. And once you see which those lines are, optimization stops being a mystery and becomes a decision.

n8n gives you that itemized receipt on every execution, without you having to configure anything. You just have to learn to read it.

Where the receipt lives: the execution and its per-node breakdown

Every time a workflow runs, n8n saves a record of that execution: which nodes ran, in what order, what data passed through each one, and —what matters to us today— how long each node took. That record is your itemized receipt.

To get to it, in n8n's left sidebar you open Executions. There you see the list of your workflows' latest runs, with their status —green if it finished fine, red if it failed—, their mode, the time, and the total time it took. That total time is the "Total: 1,180 pesos": it tells you the run was slow, but not yet on what. For the breakdown, you open a specific execution.

Inside an open execution, n8n shows you the canvas with the nodes, and the time breakdown lives in two places worth knowing:

The logs panel (Logs view), below the canvas. In recent n8n versions there's a Logs bar at the bottom of the canvas. When you open it, you see the list of nodes in the order they executed, and for each one how long it took, plus the execution's total time. It's the most convenient view for finding the bottleneck, because it puts all the times one below the other and the culprit jumps out: it's the line with the big number.

A node's detail. If you click a node to see its output, next to the Output title there's an info icon that gives you data about that node's run: when it started (Start Time) and how long it took to return results (Execution Time). It's the magnifying glass for a specific node, once you already know which one to look at.

Verify in your own panel. The exact labels and the location of the logs panel change between n8n versions, and the interface keeps evolving. What does not change is the concept: every execution records the per-node time, and somewhere in the open execution you can see it. If in your version you can't find the Logs bar, look for the time in each node's detail. This lesson's method works the same no matter where exactly your version shows the number.

One practical note before we continue, because it affects what you measure: avoid measuring with manual executions when the volume is large. When you fire a workflow by hand from the editor, n8n makes an extra copy of the data so it can show it to you on screen, and that adds time and memory the real automatic execution doesn't pay. To measure for real, look at an automatic execution —one the schedule or webhook fired in production— from the Executions list. The manual execution is for debugging the logic; the automatic one is for measuring performance.

Worked example: reading the inventory-update execution

Let's do exactly what Terra Market's operations person would do facing the eleven minutes.

Open Executions, filter by inventory-update, and open a recent run —an automatic one, of the ones the schedule fires every 15 minutes—. The total time says 11 min 4 s. That's the un-itemized ticket. Now open the logs panel to see the itemized receipt:

inventory-update execution — 14:30:00 — total 11 min 4 s

  Node                          Time
  ─────────────────────────────────────
  Schedule Trigger              0.0 s
  ERP: get sku list             1.5 s
  Loop Over Items               ⟳
    └ HTTP: GET stock (×300)    630   s   ◄──────
  Code: transform stock         3.0 s
  Loop Over Items (2)           ⟳
    └ HTTP: POST storefront     25    s
  Postgres: log run             0.5 s

Read it the way you'd read the restaurant bill. Of the 664 total seconds, 630 go to a single place: the HTTP: GET stock node that runs inside the loop, once for each sku. Everything else together —reading the sku list, transforming, pushing to storefront, logging— adds up to about 30 seconds. The wine on this bill is crystal clear.

With this receipt in front of you, the conversation changes completely. Nobody says "n8n got too small" anymore. Now they say "95% of the time goes to calling the erp three hundred times." And that sentence already contains its own solution: if the problem is three hundred calls, the solution is to make fewer calls. That's lesson 5.

What to expect. When you measure a workflow of your own for the first time, expect to find the same as Terra Market: the time is not spread evenly. Almost always one or two nodes take the vast majority, and the rest is noise. This is good news disguised as a problem: it means you don't have to optimize the whole workflow, just that one or two lines. The rule of thumb you'll confirm over and over: 80% of the slowness lives in 20% of the nodes, and often in a single one. Your job when measuring is to find that one.

The trap: the slow node is almost always waiting, not working

Here's the part that separates someone who knows how to read a receipt from someone who only sees big numbers. When you find the node that takes the time, there's a question you have to ask yourself before trying to "optimize it," because the answer completely changes what you do:

That node — is it working or waiting?

They're two totally different things that the receipt shows with the same number, and confusing them is performance trap number one.

A node that works is using n8n's processor: it walks through a long list, transforms text, calculates, sorts, deduplicates. The time you see is compute time. The nodes that work are, above all, Code nodes with heavy logic over many items, and some operations of nodes that process data in memory. For a node that works, "optimizing it" does mean making it more efficient: better algorithm, fewer loops, less data to walk through.

A node that waits sent a request to another system and is standing there until it responds. The time you see is network-wait time, and during all of it n8n's processor sits with its arms crossed. The nodes that wait are almost all the ones that talk to the outside world: HTTP Request, database nodes like Postgres, service nodes (Slack, email, the carrier), AI-model nodes. For a node that waits, "optimizing it" by making it more efficient does nothing, because the time isn't in n8n, it's on the other end of the line. The only lever is making it wait fewer times or wait for something faster.

Go back to the inventory-update receipt. The node that takes the 630 seconds is an HTTP: GET stock. Does it work or wait? It waits. It's sending three hundred requests to the erp, one after another, and each one leaves it standing there for two seconds awaiting a response. If someone tried to "optimize" that node by reviewing its configuration, changing its options, or simplifying its expression, they wouldn't gain a single second, because the two seconds per call are put there by the erp, not by n8n. The only way to lower the 630 seconds is to call fewer times.

Now comes the second face of the trap, and it's subtler: the node that looks slow is sometimes just waiting on another one that really is the culprit. This happens in two typical ways:

The node that accumulates a loop. A Loop Over Items that shows up with a huge time isn't "a slow node": it's a node that loops, and the time you see is the sum of all its iterations. The culprit isn't the loop itself, it's what's inside each turn —in our case, the call to the erp—. If you sit there staring at the Loop Over Items trying to speed it up, you're looking at the packaging; the contents —the repeated call— are the problem.

The node that merges branches. A node that waits for several branches to finish —a Merge, or any node that comes after a long path— can report that the execution reached it late, without it having taken any time. Its "time" on the wall clock is that of the slow branch that precedes it, not its own. Before accusing a node, look at whether the time is its own —what it took to run— or inherited —how long it took for its turn to arrive—.

The rule, so you don't forget it:

Finding the node with the big number is only half the work. The other half is asking whether that node works or waits, and whether the time is its own or the one that precedes it's. Optimizing without answering that is like swapping the car's engine to skip a line that isn't moving.

The three buckets, now that you know how to read them

In lesson 1 we named the three kinds of time. Now that you know how to open the receipt, you can recognize each one in the panel, which is what really matters.

Compute time — n8n is working. How it looks: a Code node (or another that processes data in memory) with a notable time, with no calls to external systems inside. How to confirm it: the node doesn't talk to anyone outside; it only transforms what it already has. What fixes it: better logic, less data to walk through, or —if it really is a lot of compute— moving the heavy work to where it belongs (a database that sorts and filters far better than a loop in Code). It's the only bucket where a faster processor helps some, and it's almost always the smallest.

Network-wait time — n8n is waiting on another. How it looks: an HTTP Request, database, or service node with a large time, especially if it's inside a loop and repeats many times. How to confirm it: the node's time is proportional to how many times it calls, not to how much data n8n processes. What fixes it: fewer calls (lesson 5), batches (lesson 3), or finding the pace the other API tolerates (lesson 6). A bigger server doesn't touch this bucket, and it's —in integration workflows like Terra Market's— the biggest bucket by far.

Queue time — the execution hasn't even started. How it looks: you don't see it inside the execution, because it isn't any node's time. It shows up in the difference between the time the event should have fired the workflow and the time the execution really started. If order-sync was supposed to fire at 21:00:03 and its execution started at 21:01:10, that minute-plus is queue: it was lined up waiting its turn. How to confirm it: it happens when there are many simultaneous executions competing for the instance. What fixes it: this is the only one of the three that is not fixed in the workflow. It's infrastructure, and it's the sign of lesson 7 —the boundary toward the self-hosting guide—.

The usefulness of the three buckets is that each one sends your effort to a different place, and the panel tells you which one you're in. If the time goes to network wait, don't even dream of buying a machine: lower the calls. If it goes to queue, then scaling might be in order —but first confirm each execution is already optimized, or you'll be scaling to run waste faster—.

The total and the sum don't always match

There's a reading detail worth learning early, because when it shows up it's confusing. Sometimes you add up all the nodes' times and it comes out less than the execution's total time. Where did the difference go?

Think about it with the restaurant bill again: your receipt's total can include a tip or a cover charge that isn't in any of the food lines. In an execution, the difference between the total and the sum of the nodes is the time that belongs to no node in particular: above all the while the execution spent waiting its turn before starting (queue), plus some internal work by the engine itself. A node can't take time while it isn't running, so that gap doesn't appear in any line.

That's why, when the total is quite a bit bigger than the sum of the nodes, it's a strong hint that you're in the queue bucket: the execution took long not because its nodes were slow, but because it took long to start. And that is, again, the sign that points to the infrastructure, not the workflow. In inventory-update this doesn't happen —its total (664 s) and its sum (660 s) almost match, a sign that the problem really is what the nodes do, not the queue—. But in an order-sync at 21:00 you could indeed see a total well above the sum, and there the conversation is a different one: lesson 7's.

Keep the reading: sum ≈ total means "the time is in the nodes, fix it here"; total ≫ sum means "the time is in waiting its turn, this smells like infrastructure."

How to make an honest measurement, step by step

Let's gather it all into a procedure you can repeat with any slow workflow of yours. The keyword is honest: a measurement you can't trust is worse than none, because it gives you false security.

Step 1 — Measure an automatic execution, not a manual one. Go to Executions and open a real production run, not one you fire yourself from the editor. Remember: the manual one pays an extra data copy that the real one doesn't, and it'll show you inflated times and memory. What to expect: if you compare a manual and an automatic run of the same workflow with the same data, the manual one usually comes out worse. Use the automatic one.

Step 2 — Read the whole receipt before reacting. Open the logs panel and look at all the times, not just the first one that catches your eye. The goal is to see how the total splits up. Almost always you'll find one or two nodes taking almost everything; note them down.

Step 3 — For the culprit node, ask whether it works or waits. It's the trap's question. Is it a node that processes data in memory (works) or one that calls outside (waits)? Is it inside a loop that repeats it? Is the time its own or inherited from a slow branch?

Step 4 — Measure more than once. A single execution can fool you: the erp might have been having a bad moment right then. Look at two or three runs from different hours. If the culprit node is the same in all of them, you have a stable bottleneck. If it changes from one to another, your problem might be variability of the external system, which is another conversation.

Step 5 — Write down the number before touching anything. Before optimizing, put in writing "today it takes X, node Y takes Z." That number is your baseline, and it's what will let you prove, afterward, that your change helped. Optimizing without a baseline is like going on a diet without ever having weighed yourself: you won't be able to say if it worked. The lesson 8 project is built entirely on this baseline.

That's the complete method, and it's deliberately boring. The glamour of optimization is in the clever fix of lesson 5; but the clever fix only works because these five boring steps pointed you to the right place.

Common mistakes

Optimizing by intuition without opening an execution (conceptual). What happens: someone "knows" which is the slow node —the one that seems complicated to them, or the last one they touched— and starts optimizing it without measuring. Sometimes they get lucky; almost always they spend hours on a node that contributed 2% of the time while the real culprit stays intact. Why it happens: measuring feels like wasting time before the "real" work, and intuition about performance is notoriously bad —even experienced engineers are often wrong guessing where the bottleneck is—. How to spot it: if you can't say "node X takes Y% of the time" with a number you read, you're optimizing blind. How to fix it: open an execution before changing anything. It's two minutes, and it saves you an afternoon on the wrong node. It's literally the module's rule: optimizing without measuring is guessing.

Confusing wait time with compute time and "optimizing" a node that only waits (practical). What happens: an HTTP Request with ten minutes is seen and someone tries to speed it up by reviewing its configuration, changing its options, simplifying the expression that builds the URL. Nothing improves. Why it happens: the big number makes you think the node "does a lot," when what it does is wait a lot for an external system. The ten minutes are put there by the erp, not by n8n. How to spot it: ask whether the node talks to the outside world. If yes, its time is wait, and no optimization of its configuration lowers it. How to fix it: the lever of a node that waits is how many times it waits —reduce calls (lesson 5) or group them into batches (lesson 3)—, never "tuning it."

Blaming the loop instead of what's inside it (practical). What happens: a Loop Over Items with a huge time is seen and the conclusion is "the loop is slow, we have to remove it or change it." But the loop doesn't take long; what takes long is what's done inside each turn. Why it happens: the time appears associated with the loop node because it's the sum of its iterations, and it's easy to read that as "the loop is the problem." How to spot it: look at what runs inside the loop. If it's an external call repeating N times, the problem is the N calls, not the loop mechanism. How to fix it: don't remove the loop; reduce what each turn does or —better— replace N one-at-a-time calls with a few by-batch calls (lessons 3 and 5).

Measuring with a manual execution and drawing performance conclusions (practical). What happens: the workflow is fired by hand from the editor to "see how long it takes," a number comes out, and decisions are made with it. The number is inflated, because the manual execution pays an extra data copy for the screen. Why it happens: it's the most convenient thing —you hit "execute" and see the time— and not everyone knows the manual and automatic ones don't cost the same. How to spot it: if your measurement comes from hitting "execute" in the editor, be suspicious of it, especially if there's a lot of data. How to fix it: measure on an automatic execution from the Executions list. Keep the manual one for debugging the logic, not for measuring the speed.

Measuring once and taking it as eternal truth (conceptual). What happens: an execution is opened, it's seen to have taken long, and it's assumed that this is "the workflow's time." But that run might have fallen right when the erp was having its worst moment of the day. Why it happens: a single measurement feels like a fact, and it's barely a sample. How to spot it: did you look at one run or several from different hours? With just one you don't know if it's the stable bottleneck or a bad day for the external system. How to fix it: look at two or three executions from different moments. If the culprit is always the same node, it's structural and worth fixing; if it varies a lot, your problem might be the instability of the other system, and that's handled differently.

Exercises

Exercise 1 — Read the receipt. You're given the breakdown of an execution of a Terra Market workflow that builds a daily report. Say where the bottleneck is, whether that node works or waits, and what you would NOT do.

Total: 4 min 12 s
  Schedule Trigger .............. 0.0 s
  Postgres: read yesterday ....... 2.1 s
  Code: build report rows ........ 6.0 s
  Loop Over Items ⟳
    └ HTTP: enrich each row (×140) 235 s
  Code: format ................... 1.5 s
  Email: send report ............. 3.0 s
See solution

The bottleneck is crystal clear: the HTTP: enrich each row, inside the loop, takes 235 of the execution's 252 seconds (more than 93%). Everything else is noise.

That node waits: it's an HTTP Request calling an external service once per row, 140 times. Its time is network wait multiplied by 140 calls.

What I would NOT do: spend a single minute on the Code: build report rows, even though it has 6 seconds and is the second-biggest number. Six seconds isn't the problem when the neighbor has 235. Nor would I try to "tune" the HTTP Request itself, because its time isn't in its configuration, it's in the 140 calls. The right direction is to reduce those 140 calls to a few by batch —if the service offers enriching several rows at once— or, if it doesn't, see if any of that enrichment can be resolved without calling (lesson 5).

Why it works: the receipt makes obvious what without it would be a debate. Nobody would propose buying a server looking at this: it's clear that n8n spent four minutes waiting on a third party 140 times.

Exercise 2 — Works or waits. For each node, say whether its time is compute (works) or network wait (waits), and therefore whether "making it more efficient" makes sense or not.

(a) A Code node in All Items mode that walks through 80,000 records to group them by store_id, and takes 9 seconds. (b) A Postgres node that does one INSERT for each of 500 items, inside a loop, and takes 40 seconds. (c) An HTTP Request node that calls the carrier a single time and takes 6 seconds because the carrier is slow today. (d) A Code node in Each Item mode that formats a date, runs 500 times, and adds up to 0.3 seconds.

See solution

(a) Compute. n8n really is working: eighty thousand records walked through in memory. Here "making it more efficient" does make sense —better logic, or moving the grouping to the database, which groups far better than a loop—. It's real compute, though 9 seconds is rarely a workflow's biggest problem.

(b) Wait. It's 500 round trips to the database, one per item. The time isn't in n8n, it's in the 500 connections. "Tuning" the node doesn't help; what helps is doing one INSERT with the 500 values at once (many databases allow it), which is exactly reducing calls.

(c) Wait. The 6 seconds are put there by the carrier, not n8n. And note: it's one call, so there's no "reduce calls" to do. If the carrier is slow today, no workflow optimization fixes it; it's variability of the external system. The only thing within your reach would be a reasonable timeout so it doesn't hang you if one day it takes too long.

(d) Compute, but irrelevant. It works, yes, but 0.3 seconds across 500 runs is nothing. The lesson of this item: not everything that "works" is worth optimizing. Size rules. A node that works a lot and takes little isn't a bottleneck.

Why it works: the question "works or waits" tells you what kind of lever to apply, and the size of the number tells you whether it's worth applying it. You need both: direction and magnitude.

Exercise 3 — The inherited-time trap. A colleague opens an execution and tells you: "the Merge node is the slowest, it takes 3 minutes, we have to optimize the Merge." You look at the workflow: the Merge joins two branches; one branch is an instant Set and the other is an HTTP Request to the erp that takes 3 minutes. Is your colleague right? What would you explain and what would you look at?

See solution

Your colleague is reading the receipt wrong. The Merge doesn't take 3 minutes working: it takes 3 minutes because it can't join the two branches until the slow branch —the HTTP Request to the erp— finishes. That time is inherited, not its own. The Merge is waiting for its turn to arrive, just as you "take" half an hour at a restaurant even though your dish cooks in ten minutes, because you spent the time waiting for it to arrive.

What I'd explain: a node that joins branches inherits the time of the slowest branch that feeds it. Optimizing the Merge is impossible because the Merge doesn't do anything slow; the slow work is upstream, in the HTTP Request.

What I'd look at: the real node of the 3 minutes is the HTTP Request to the erp. There I apply the usual questions: is it one call or many? If it's one, is the erp slow (variability, little within my reach) or am I requesting too much data at once? If it's many, reduce calls.

Why it works: distinguishing your own time from inherited time saves you from the classic error of "optimizing the last node before the result," which is usually innocent and was only waiting. The culprit is almost never at the end; it's upstream, in whoever really took the time.

Summary and next step

In this lesson you learned to read a workflow's itemized receipt instead of looking only at the total. You saw where it lives: in the Executions list, opening an execution and looking at the logs panel or each node's detail, where n8n tells you how long each took. You applied the reading to the Terra Market case and found that of inventory-update's eleven minutes, 630 seconds go to a single node: the call to the erp repeated three hundred times. And you learned the trap that fools almost everyone: the big number isn't enough, you have to ask whether that node works or waits —because a node that waits isn't sped up by tuning it, only by making it wait fewer times— and whether its time is its own or inherited from a slow branch that precedes it. You recognized the three buckets in the panel —compute, network wait, queue— and what fixes each. And you kept the honest five-step method: measure an automatic execution, read the whole receipt, ask works-or-waits, measure several times, and write the baseline before touching anything.

Before moving on you should be able to: open an execution and say which node takes the time with a number; explain why a slow HTTP Request isn't fixed by tuning it; and name the three buckets and which one isn't fixed in the workflow.

Now that you know where the time goes, the fixing part begins, and the first technique attacks one of the problems measurement brings to light most often: what you do when not three hundred, but ten thousand items come in through a node at once. Lesson 3 is about batch processing: splitting a large volume into manageable parts with Loop Over Items, and —most interestingly— why choosing the size of those parts is a decision with two costs that pull in opposite directions.

Resources