If you read the NocoBase workflow docs the way I did, you come away thinking workflows can't run concurrently. Two sentences do most of the damage:
Workflows can be triggered concurrently, but they are executed sequentially in a queue. Even if multiple workflows are triggered at the same time, they will be executed one by one, not in parallel.
— ExecutionsRegardless of the mode, each branch will be executed in order from left to right.
— Parallel branch
Taken at face value, that reads as "workflows have no throughput parallelism, and even parallel branches run one after another." If it were true, it would be a serious constraint on how you design anything with real load.
So I built the three scenarios on my own instance and measured them. My prediction was wrong three times in a row. The short version: everything runs in parallel, cleanly.
Test setup: NocoBase 2.1.23 (official Docker image) + PostgreSQL 16, on a 12-core machine. Workflows were created, triggered, and measured entirely through the REST API. Elapsed time is the difference between
createdAtandupdatedAton rows in theexecutionstable — not a stopwatch.
Test 1: two "wait 5 seconds" branches under a parallel node
I put a Parallel branch node in front of two branches, each with a single 5-second delay node.
- Sequential → 10 s
- Parallel → 5 s
Result:
status = 1 (success) / elapsed 5.17 s
5 seconds. The two waits overlapped in real time. Looking at the job records, the two delay nodes started 11 ms apart:
parallel branch created = 06:25:01.894
delay A (5s) created = 06:25:01.904
delay B (5s) created = 06:25:01.915 <- did not wait for branch A to finish
So "executed in order from left to right" is about the order branches are entered, not the order they finish. The moment branch A parks itself on a wait, the engine hands control back and branch B starts. The waits stack.
Test 2: surely CPU work runs sequentially?
Fine, waits overlap. But Node.js is single-threaded, so CPU-bound work shouldn't overlap — that was my next assumption. I put a JavaScript node running a 3-second busy loop on each of two branches.
const t = Date.now();
while (Date.now() - t < 3000) {} // hog the CPU for 3 seconds
return 'done';
- Sequential → 6 s
- Parallel → 3 s
Result:
status = 1 (success) / elapsed 3.18 s
both branches returned result = 'done'
3 seconds again. Even CPU work overlapped.
The trick is in the plugin. The JavaScript node doesn't run on the main event loop — it runs in a worker_threads worker:
// from @nocobase/plugin-workflow-javascript
var import_node_worker_threads = require("node:worker_threads");
Because it runs on a separate thread, a CPU-bound script doesn't block the main loop. "It's Node, so it's single-threaded" was exactly the wrong intuition here.
Test 3: fire four executions at the same time
This is the one the docs speak to most directly: "even if multiple workflows are triggered at the same time, they will be executed one by one."
I triggered the same 3-second-CPU workflow four times simultaneously.
- Sequential → 12 s
- Parallel → 3 s
Result:
id | status | started | finished | elapsed
------+--------+--------------+--------------+---------
...2 | 1 | 15:38:53.341 | 15:38:56.834 | 3.493
...3 | 1 | 15:38:53.395 | 15:38:56.782 | 3.387
...4 | 1 | 15:38:53.404 | 15:38:56.811 | 3.407
...5 | 1 | 15:38:53.408 | 15:38:56.823 | 3.415
wall-clock for all four: 3.49 s
All four ran fully in parallel. They started within ~70 ms of each other and finished within ~50 ms of each other. (Three 5-second delay executions fired together came in at 5.1 s wall-clock, same story.)
So are the docs wrong?
No — and this is the part worth slowing down on. The same Executions page also says, in effect: a "Running" execution that is parked on a waiting node is not holding the dispatch slot. Other queued executions can start while it waits.
In other words, "sequential" describes the order the dispatcher pulls from the queue, not "only one runs at a time." As soon as an execution hits a node that yields — a delay, a manual approval, an HTTP request — the next one starts. And JavaScript nodes escape to a worker thread entirely.
But I'll say this plainly: the English phrasing "one by one, not in parallel" reads as a throughput claim, and it made me design around a limit that isn't there. I misread it completely. Worth knowing: the Chinese-forum reports about "the queue getting stuck" (e.g. t/10331) are all from the 1.x era. In 2.x the dispatcher was reworked — PR #9673 fixed duplicate dispatch, PR #9953 eased lock contention — and it just runs concurrently.
Bottom line: you don't need to worry about 2.x workflow concurrency. Waits overlap, CPU work overlaps, simultaneous triggers overlap.
The real traps are somewhere else
Concurrency turned out to be a non-issue. But there are constraints that do bite in production — and the docs state them outright. These matter far more than the parallelism question.
1. Bulk operations don't fire events
"Collection events do not currently support triggering by bulk data operations."
— Collection event
And:
"using the SQL action node to operate on the database is equivalent to direct database operations and will not trigger collection events."
A workflow you thought you wired up silently does nothing depending on the path that changed the data. CSV bulk import or an update via the SQL node won't trigger it. If you put business logic in workflows, this is the biggest footgun.
2. Missed schedules never recover
"if ... the entire NocoBase application service is in an inactive or shutdown state, the scheduled task ... will be missed. Moreover, after the service is restarted, the missed tasks will not be triggered again."
— Schedule event
Any scheduled run that lands during a deploy or upgrade restart is silently dropped — and never replayed. If a daily batch lives in a workflow, mind your restart windows.
3. Loop limit defaults to unlimited
You can cap it with the WORKFLOW_LOOP_LIMIT env var, but the default is unlimited (Loop node). Get a loop condition wrong and nothing stops it. Set this in production.
4. No failure notifications
To a forum question — "my scheduled workflow errors out sometimes, there's no alert, and I only find out by digging through the history" — the official reply was:
"你好,目前没有" (currently, there is none)
— forum t/12040
Put something critical in a workflow and it can fail quietly. You have to build the monitoring yourself.
5. The transaction node arrived in 2.2.0
The DB transaction node — the one that rolls back a group of operations together — was added in 2.2.0 (Transaction node).
Which means 2.0 / 2.1 workflows have no all-or-nothing guarantee. Take a workflow that "① decrements stock → ② writes a shipment record." If ② fails, ① stays applied. Stock went down, no record exists — data left in a half-broken state. If you're running anything where integrity is the whole point (money, inventory) on 2.1 or earlier, keep this in mind.
Takeaways
-
Concurrency was never the problem (measured on 2.1.23). Parallel branches, and multiple simultaneously-triggered executions, overlap cleanly — for both waits and CPU work. JavaScript nodes run on
worker_threads, so the single-thread intuition doesn't apply. - The official "sequentially in a queue / one by one, not in parallel" is about dispatch order. Read as a throughput claim, it misleads. (It misled me.)
- The Chinese-forum "queue gets stuck" reports are all 1.x. The 2.x dispatcher was rebuilt.
- The real traps are "won't fire / won't notice / breaks halfway" — bulk ops and the SQL node skip events, schedules are dropped on restart, failures are silent, and pre-2.2 has no transaction node so a mid-workflow failure leaves data inconsistent.
Reading one sentence of docs and designing around it cost me more than spinning up Docker and measuring would have. The environment is one docker compose up away — if something about your workflow design worries you, measure it.
(Measured on 2.1.23 / PostgreSQL 16 / 12 cores. Worker-thread count tracks CPU cores, so on a small box the CPU-parallelism will be lower.)
Top comments (0)