docs/install/architecture/latency.mdx
A synchronous webhook holds the connection open until your flow returns, so the caller waits for the orchestration around your flow plus your flow's own work. These are floor numbers, slow paths included.
<Note> **Scope:** the recommended production setup (`SANDBOX_CODE_ONLY`, `AP_REUSE_SANDBOX=true`, one flow per worker), which self-hosted deployments and **dedicated Cloud workers** run. The shared Cloud freemium pool uses a different sandbox. See [Production Setup](/install/configure-operate/production-setup). </Note>| Situation | Caller waits |
|---|---|
| Warm (the normal case) | ~0.2 s |
| Cold (first request on a fresh worker) | ~2 s |
| Under heavy load (every worker busy) | ~0.5 s |
| Timeout (flow never responds) | 30 s, then the connection closes |
Cold means a worker that has never seen this flow: it installs the pieces, fetches the bundle, and forks an engine first. Warm reuses all three. You hit cold when:
AP_REUSE_SANDBOX is off (then every request is cold).Every run records where its wall-clock time went, shown as a four-phase bar. To view it, open any run and hover the info icon next to the run's duration ("Took") in the run header.
The phases run in order:
| Phase | What it covers | Warm | Cold |
|---|---|---|---|
| Queue | Waiting after the run is created until a worker picks it up and starts setup — contention (every worker busy) plus dispatch overhead. | ~0 | grows under load |
| Setup | Fetching the flow bundle and installing the pieces, engine, and code steps into the sandbox (provisioning). | ~0 (cached) | ~0.5 s |
| Warm-up | Forking the engine process, Node boot, isolated-vm init, and the socket handshake (engine boot). | ~0 (reused) | ~0.9 s |
| Run | Executing the flow's trigger and steps — the same number reported as the run's duration (Took). Your own work and third-party calls dominate here. | your flow | your flow |
Queue, Setup, and Warm-up are the orchestration around your flow; Run is the flow itself. On a warm worker the first three collapse toward zero — the reused sandbox is already booted, so Warm-up is ~0 — and the bar is almost entirely Run, which is why warm calls are dominated by your own work.
For a flow that does real work — a step calling a third-party API — that outbound call dominates Run, and the Activepieces overhead is the smaller number next to it.
For a bare flow (no external calls), Run is mostly persisting the run, not executing it. A production run makes a fixed set of network round-trips to the app and object store, independent of step count:
Per-step progress callbacks fire only for test runs in the builder, not production webhook runs — so adding steps does not add a callback per step to a live run.
The practical consequence: a bare warm run's latency tracks your object-store proximity more than anything else. Keep the object store in-region and reachable over a private, same-zone path.
Four-step flow (webhook, math, code in isolated-vm, response), single warm call, no contention:
| p50 | avg | p95 | p99 |
|---|---|---|---|
| 163 ms | 176 ms | 260 ms | 343 ms |
AP_REUSE_SANDBOX=true plus a statically sized, always-on fleet.