Performance
Throughput and latency belong to an installation, not to the software: they are decided by the flows you publish, the backends they call, the hardware underneath and a handful of settings. This page says what those are and how to measure them, so the numbers you quote are yours and reproducible.
What one delivery costs
A synchronous delivery through /run does this, in order:
- The gate. Two indexed lookups — the tenant row by primary key, then the key hash — and the size ceiling for this access point, all inside one database step, before a byte of the body is read.
- The body. Read up to the ceiling, then parsed into the canonical value — JSON, or XML when the content type says so.
- The steps. Executed in order, on one thread. A
splitfans out to threads and joins; aforkdetaches. Everything else is sequential. - The waits. Each
http_egressorgrpc_egressstep is a round trip to somebody else. On any flow that calls out, this is the number. - The record. One
receivedand one verdict event onto a bounded channel, written by the logging thread to SQLite and to the day’s JSON Lines file. Plus a dedup or correlation row if the flow declares one.
Two consequences follow. Latency is mostly not ours: on a forwarding flow, the platform’s share is parse, transform and record, and the rest is the backend. And concurrency is across deliveries, not inside one: one delivery uses one thread until it finishes or waits, so the number that matters for throughput is how many deliveries may run at once.
The settings that bound it
| Setting | Where | What it bounds |
|---|---|---|
max_concurrent_flow_executions |
system_config |
Deliveries running at once, across all tenants. Past it, /run answers 503 with Retry-After |
max_queue_workers, max_queue_workers_per_tenant |
system_config |
How many distinct queues can drain on this server — a count of queues, not a rate |
flow_max_duration_secs |
system_config |
Wall time of one execution, checked between steps |
max_message_bytes |
system_config, frontmatter, egress step |
The largest body accepted or sent at one access point |
rate_limit_rps |
per API key | The caller’s own ceiling, as a token bucket with twice that as burst |
log_level, log_payload_max_bytes |
flow frontmatter | Whether payloads are written alongside events, and how much of them |
All of these are described in Running NexusFabric and
Limits. Most are read at startup, so a change takes effect on the next restart;
max_message_bytes is read per request.
The two doors measure different things
/run measures the whole integration: the caller waits, the backend’s latency is inside the number,
and saturation is visible as 503 with Retry-After.
/enqueue splits it in two. Acceptance is a bounded read plus an append to the flow’s write-ahead
log, answered 202; the work happens later, in the worker that drains that queue. So an installation
on the async door has two numbers — an acceptance rate and a drain rate — and measuring only the
first tells you nothing about whether the queue is keeping up. Read the drain rate from the
queue_processed events, or watch the depth on the Queue page.
Measuring an installation
Measure the flows you are going to run. A flow that forwards to a stub answering instantly measures the engine; the same flow against your real backend measures the integration. Both are useful, and they are different numbers — say which one you took.
Fix and record everything that moves the result. A figure without these is not comparable with the next one:
- the product version and the artifact hash of each flow under test;
- cores and memory of the machine, and whether the registry, queue and log databases are on local disk or on a network filesystem;
- the message size and its shape — a 2 KB JSON body and a 2 KB SOAP envelope do not cost the same;
- the concurrency the driver holds, and whether it is open or closed loop;
- the backend’s own latency during the run;
log_levelfor each flow, and whether payload logging was on;- whether the gate was on, and whether the key under test had a rate limit.
Drive it with a real HTTP load tool, giving it the key in the Authorization header. Discard a
warm-up: the first calls to a flow pay the artifact read and the connection setup.
Read the result from three places, and expect them to differ:
- the driver, for client-side latency and error rate — this is the number a caller experiences;
- the
duration_msfield on eachcompletedevent in the structured log, for the execution alone, without transport or queueing at the socket; - the Stats page of the management UI, for volume, errors, latency and the busiest flows over the whole run.
One rule about errors. A run with transport errors in it is not a slow run, it is a failed run; fix the cause and measure again rather than reporting a rate next to a footnote.
For a quick mixed load that exercises both doors and fills the logs, statistics and audit trail,
nexus simulate drives the flows that nexus seed publishes, in quiet, steady, burst and spike
phases. It is a way to see the installation work, not a harness for your own flows — for those, use
your own driver. See Command line.
Tuning, in the order worth trying
- Turn payload logging down.
log_level: event_onlyis the default for a reason: atpayload_trimmedorfullevery delivery writes its body twice, to SQLite and to the JSON Lines file, and the masking pass runs over it first. - Raise the concurrency ceiling if
503appears while the CPU is idle and the backend has room. Leave it alone if the backend is the thing that is full — refusing early is better than queueing inside a caller’s socket. - Count your queues.
max_queue_workersmust exceed the number of distinct(tenant, flow)queues you expect to drain, however quiet each one is, andmax_queue_workers_per_tenantis inert until you set it. - Check the key’s rate limit. A per-key ceiling caps what you can measure. Remove it for the measurement, and put it back afterwards.
- Move the wait. A flow whose backend is slow but whose caller does not need the answer belongs
on
/enqueue, with retry and a dead-letter queue, rather than holding a connection open. - Add engines. Several engines over one shared state directory, behind the proxy that already terminates TLS — see High availability.