Limits and constraints
Every bound the platform enforces, with its default and where you change it. Then the behaviours that are true, deliberate, and likely to cost you an afternoon if you assume otherwise.
Limits
Message size
| Limit | Default | Configurable |
|---|---|---|
Request body, /run and /enqueue |
1 MiB | Yes, on three levels — see below. |
Inbound gRPC message, on the door opened by --grpc-port |
1 MiB | Yes, the same three levels — the flow’s own max_message_bytes: wins over the platform default. |
| gRPC request message, encoded on the wire | the installation ceiling, so 1 MiB unless the flow raises it | Per step, max_send_bytes. |
| gRPC response message, encoded on the wire | the installation ceiling, so 1 MiB unless the flow raises it | Per step, max_recv_bytes. |
| Protobuf descriptor file | 8 MiB | No. |
A body over the limit is rejected before it is read, not after: the ceiling is resolved from the
registry before the first byte is pulled, so a rejected 100 MB request does not cost 100 MB of memory
(ADR-030 §D2). A declared Content-Length over the limit is refused without pulling a single chunk;
a body without one stops at the first chunk that crosses it.
The three levels, innermost winning: max_message_bytes: on an http_egress or grpc_egress
step, then max_message_bytes: in a flow’s frontmatter — which is the ceiling for that access
point — then max_message_bytes in system_config, which is the platform default of 1 MiB. No
value anywhere means unlimited; an absent or unparseable one falls back to the default, and 0 in
frontmatter is a parse error rather than a silent “no limit”.
max_send_bytes and max_recv_bytes start from that same ceiling. A gRPC step that sets
neither sends and receives up to whatever max_message_bytes resolves to at that point, so a flow
that raises its ceiling raises its gRPC limits with it and does not have to say so twice. Setting
either key on the step overrides it for that direction only. Before this, both defaulted to a fixed
4 MiB that no setting could reach; a flow declaring a 100 MiB ceiling still could not send more than
4 MiB, and one relying on the old 4 MiB now gets the installation default of 1 MiB unless it raises
it.
A request message over max_send_bytes is refused before any connection is opened. The encoded
length is measured against the limit ahead of the endpoint cascade, so an oversized message costs no
handshake, touches no circuit breaker, and reaches no server. The step fails with a message naming
the measured size, the limit, and which key to change.
A flow that declares a ceiling above inline_payload_max_bytes and also transforms, validates
semantically, or logs at full is refused at nexus deploy, naming the step. The check runs at
deploy rather than at compile or validate, because the threshold it compares against is a
platform setting and not a property of the file.
That refusal is the whole of the protection today, and it is worth knowing why it exists. Above the threshold a message is meant to be transport only — carried without being parsed into a value. That handling is not built yet: below the ceiling, every body is still materialised and parsed. So raising the ceiling to 100 MB buys you the transport at a memory cost of the same order as the body, and the deploy-time refusal is what keeps a flow from combining a raised ceiling with the steps that would multiply it.
The gRPC limits are applied on the wire, before decoding — a limit checked after decoding would be
measuring memory that has already been allocated. That holds on the inbound door too: the ceiling
reaches the decoder before it decodes, and a message over it never reaches the flow. It leaves a
delivery_refused event on channel grpc carrying the reason, so a ceiling reached is never a
silence.
gRPC streams
A server-streaming call is collected in full before the step returns, so the per-message limit says nothing about the total. Three bounds cover the collection.
| Limit | Default | Configurable |
|---|---|---|
| Messages collected from one stream | 10 000 | Per step, max_stream_messages. |
| Total encoded bytes collected from one stream | the larger of max_recv_bytes and the installation ceiling |
Per step, max_stream_bytes. |
| Wall time for the whole call, headers and stream body included | 30 s | Per step, deadline_ms. |
Exceeding any of the three fails the step. The messages collected so far are discarded, not returned. This is deliberate: a truncated array handed over as if it were complete is a wrong answer the flow has no way to detect, whereas a failed step goes to the fault handler.
Note that max_stream_bytes follows max_recv_bytes, and never falls below the installation
ceiling. Raising the per-message limit raises the stream total with it unless you set both; lowering
max_recv_bytes below the ceiling does not lower the stream total, which would otherwise have let
one key silently bound the other in the direction nobody asked for.
Deadlines and timeouts
| Limit | Default | Configurable |
|---|---|---|
| HTTP egress TCP connect | 10 s | Per step, connect_timeout_ms. |
| HTTP egress total request | 30 s | Per step, read_timeout_ms. |
| gRPC TCP connect | 10 s | Per step, connect_timeout_ms. |
| gRPC total call | 30 s | Per step, deadline_ms. |
| Parallel fan-out wall time | none | Per step, timeout_ms in the split fence. |
| Wall time of a whole execution | 2 h | flow_max_duration_secs, server-wide. |
The split timeout is a check made after every branch has finished, not a cancellation: branches
are never interrupted, and the step fails only if the total exceeded the budget. 0, which is the
default, disables the check.
The execution ceiling is checked between steps, not enforced during one. A step that has started
runs to completion — nothing here is interruptible — so what the ceiling bounds is how many further
steps begin. A flow that reaches it fails with 500 and a body naming the step that did not start.
Steps that already ran produced their effects, and none of it is rolled back; see
An execution runs to completion.
Two consequences worth planning around:
- A single very long step is not bounded by this. The ceiling sits between steps, so one call
with a two-hour
read_timeout_msreaches the far end regardless. - A flow that overruns and has a
## Fault:section can spend up to twice the ceiling. The fault sequence is given its own allowance, so that a flow can still report its own failure at the moment its budget ran out.
A value that is not a number is not the same in both places. On a gRPC step a malformed
max_recv_bytes or deadline_ms fails the step rather than falling back to the default — a
limit written to be strict must not be relaxed by a typo. On a split fence a malformed
timeout_ms becomes 0.
HTTP headers
| Limit | Default | Configurable |
|---|---|---|
| gRPC response headers and trailers, combined | 64 KiB | No. |
| Outbound custom header name | must be an RFC 7230 token: letters, digits, and ``!#$%&’*+-.^_` | ~`` |
| Outbound custom header value length | no platform-imposed bound | — |
The gRPC header bound exists because headers and trailers sit outside the message-size limit; a server could respect a small body limit and still make the process allocate megabytes of trailers.
Some header names cannot be set on an outbound call at all, and a conflicting pair is a compile error. See HTTP requests.
Queue and retries
| Limit | Default | Configurable |
|---|---|---|
| Delivery attempts | 3 | Per flow, queue_max_attempts. |
| Delay before the second attempt | 1000 ms | Per flow, queue_initial_delay_ms. |
| Backoff multiplier per attempt | 2.0, minimum 1.0 | Per flow, queue_backoff_factor. |
| Ceiling on any single delay | 60000 ms | Per flow, queue_max_delay_ms. |
| Worker poll interval while the queue is idle | 500 ms | No. |
| Workers per queue | 1 | No. |
With the defaults: three attempts, delays of 1 s and 2 s, then the dead-letter queue. A value
below 1.0 for queue_backoff_factor is rejected by nexus validate. See
Queues, retries and the DLQ.
Logging
| Limit | Default | Configurable |
|---|---|---|
Recorded payload size at log_level: payload_trimmed |
4096 bytes | Per flow, log_payload_max_bytes. |
Recorded payload size at log_level: full |
unbounded | — |
| Pending events held in memory before they are written | 4096 | No. |
| Retention of log rows in the database | 30 days, minimum 1 | Platform-wide, from the Settings page. |
| Retention of the daily JSON Lines files | 30 days, minimum 1 | The same setting as the rows. The current day’s file is never removed. |
Over the payload cap, the payload becomes a truncated string prefixed with [TRIMMED:<n>B] — the
event is still recorded, and masking has already been applied. When the in-memory buffer is full,
events are dropped rather than slowing the flow. A flow whose output is an XML document records no
payload at all, at any level, while still recording the event. Logging never fails a request. See
Logging and audit.
Rate limiting
Per API key, and only if the key was created with --rate-limit.
| Property | Value | Configurable |
|---|---|---|
| Sustained rate | rps requests per second |
Per key, --rate-limit. |
| Burst ceiling | 2 × rps |
No — it is always twice the sustained rate. |
| Tokens available at first use | rps |
No. |
| Cost per request | 1 token | No. |
A key created with --rate-limit 10 can spend 10 requests immediately, sustains 10 per second, and
after two idle seconds can burst 20. Buckets are held in memory and reset when the process
restarts. See Authentication.
Constraints
Things that are true by design and will surprise someone.
Steps run in file order, top to bottom. There is no dependency graph and no reordering. A step that needs a value produced by another must appear after it.
A condition fence that evaluates to false stops the flow silently. No error, no fault handler,
no indication in the response. On /run you get 200 with whatever the payload was at that point —
the remaining steps simply did not run. On a queued message it is acknowledged and dropped. If you
want a caller to be told, use a validate fence, which rejects with a status code.
An unknown step key is an error, checked per effect. A key nothing will read is refused at
nexus validate, with the closest match offered — and a key belonging to an effect the step did
not declare names that effect instead. Front matter is checked separately: a key close to a known
one is reported, but a key close to nothing is still accepted silently, which is what lets a
connector keep its own keys in a flow file.
Client-streaming and bidirectional gRPC methods are refused. One step supplies one request message; the step fails with a message naming the method rather than sending a partial request. Unary and server-streaming work.
A compiled artifact is immutable. Changing a flow means deploying a new version — a version
number cannot be reused, and the stored bytes of an old one never change. Every request resolves
latest at call time, so a deploy takes effect on the next request without a restart. Adding or
changing a schedule: is the exception and needs one.
A gRPC descriptor is deployed, not compiled in. Flows reference a descriptor by name and version and resolve it at call time, so a contract change is a descriptor deploy rather than a recompile of every flow that uses it.
An unknown ctx. reference evaluates to null. It is not an error. ctx.correlaton_id yields
null, and interpolating null into a template produces an empty string — so a typo shows up as a
missing value in the output, not as a failure.
split starts one thread per item. There is no pool and no cap. Fan out over an array whose
length you do not control and you decide how many threads the process creates.
A running execution cannot be cancelled. A caller that disconnects, times out, or is cut off
by a gateway does not stop the flow: it runs to its last step and performs every effect. A timeout
means no answer arrived, not that nothing happened, so a caller retrying after one must assume the
first attempt succeeded and send a key the receiving system can deduplicate on. The platform’s own
deduplication is opt-in per flow (dedup_key:) and bounds repeats of the same delivery; a flow
that does not declare it is not deduplicated at all. See
An execution runs to completion.
Nothing in a flow is transactional. A flow that fails at step 4 has already performed the effects of steps 1 to 3, and the next delivery attempt performs them again.
Queue delivery is at-least-once and only first delivery is ordered. A rescheduled message returns after messages submitted later. Build for redelivery; see Queues, retries and the DLQ.
The masking list includes ordinary field names. id, key, value and code are always
masked in recorded payloads, and if a matching key holds an object the whole subtree is replaced.
Seeing "***" in the log does not mean the field was empty.
API key checking is on by default, and --no-auth is the only way past it. With an empty key
table every authenticated route refuses every caller — correct, and it looks like a broken platform,
so the server warns about it at boot. There is no bootstrap key. See
Authentication.
There is no TLS on the inbound side. Terminate it in front of the process.
A private certificate authority can be named on a gRPC call and not on an HTTP one. A gRPC step
takes a tls_ca_cert path, read at call time so a rotation needs no redeploy. An https:// HTTP
egress step validates against the host’s trust store only.