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. |
XML nesting depth, on /run, a SOAP Body and /enqueue |
100 elements | 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 ceiling above inline_payload_max_bytes needs body: opaque or a higher threshold. A flow
that declares a max_message_bytes: above the installation’s inline_payload_max_bytes (1 MiB by
default) and parses its body is refused at nexus deploy, with a message that names the ceiling,
the threshold and the remedies. A flow that only forwards its body can declare body: opaque, and
the body is then relayed byte for byte, not parsed: see
Relaying a body unread.
A gRPC flow cannot declare body: opaque (a flow with inbound: grpc or a grpc_egress step), so
its message says so and names only the other two remedies. Either flow can lower
max_message_bytes to the threshold or below, or have the operator
raise inline_payload_max_bytes to the declared ceiling or above. The check runs at deploy rather
than at compile or validate, because the threshold it compares against is an installation
setting and not a property of the file.
Below the threshold a parsed body is still materialised and parsed whole, so its memory cost is of the order of the body. An opaque body is held once, as bytes, for the length of the request.
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.
An XML body nested deeper than 100 elements is refused, whatever its size. Only elements count:
the root is level 1, a self-closing element is a level, and in a SOAP request the count starts inside
Body. /run and /enqueue answer 400 with invalid XML body: XML nesting deeper than 100 elements; a SOAP request gets a SOAP Fault, 500 with soap:Client on SOAP 1.1 and 400 with
soap:Sender on SOAP 1.2. The refusal leaves a WARN and a delivery_refused event (reason
xml_too_deep) on channel http that carry the limit and nothing of the body. The ceiling is fixed, not a setting: the size limit above does not
bound depth, since 1 MiB holds far deeper documents than the platform can safely hold as a value. An
XML response to http_egress past the ceiling does not fail the step; it reaches the flow as text
(see http_egress). JSON is bounded by its parser at 127 levels (/enqueue accepts a body nested 126 levels, because the queue wraps it in one more level and must be able to read the frame back), and
a protobuf message at 100.
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
What stops a started execution.
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 Outbound HTTP calls.
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. |
| Longest wait of a queued message | none (no term) | Per installation, queue_retention_hours in system_config; per flow, queue_retention_hours, which overrides it. Whole hours, at least 1; 0 is refused. A message older than the term is not delivered: it moves to the DLQ with the reason expired, and stays there until it is replayed or discarded. |
| 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 | 65,536 | 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.
The retention setting covers the log and reaches no further. It does not prune the queues, the
registry or the audit chain, and flow state is purged on its own setting,
flow_state_retention_hours. See
What log retention covers.
Rate limiting
Per API key, and only if the key was created with --rate-limit; a rate of 0 is refused at
creation, so omit the flag for no limit. Per OIDC subject (sub), at the
rate the issuer’s declaration sets: nexus tenant oidc set takes --rate-limit or an explicit
--unlimited, and a declaration with neither is refused.
| Property | Value | Configurable |
|---|---|---|
| Sustained rate | rps requests per second |
Per key, --rate-limit. Per issuer, --rate-limit or --unlimited on nexus tenant oidc set. |
| Burst ceiling | 2 × rps |
No — it is always twice the sustained rate. |
| Tokens available at first use | rps |
No. |
| Cost per request | 1 token on /run, /enqueue and gRPC, and a second one when the request is then refused for lacking the flow’s required_scope; GET /flows/{tenant}/{name} and POST /connector/events/... spend one only on a refusal that names the caller |
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.
Each engine keeps its own buckets. A key configured at r requests per second can spend up to N × r across N engines, so size the ceiling for the engine, not for the installation. See Running more than one engine.
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. An ntd transform template writes it as null where text surrounds the {{ }};
endpoint: writes nothing, "", there, and an endpoint: that renders empty fails the step;
dedup_key:, correlation_open: and correlation_close: refuse a key in which one {{ }}
renders empty. So in a transform template, or in an endpoint: with text around the {{ }}, a
typo shows up in the output, not as a failure; in a dedup_key: or a correlation key it fails the
delivery, and so does an endpoint: that renders empty.
split runs at most fanout_max_parallel items at a time (10 unless the installation sets
it, at most 256). A longer array runs in waves and takes longer, not more threads. A split does
not nest: publication refuses a split whose called flow has a split, foreach, fork,
include or call_flow in its main step sequence. One split never runs more than
fanout_max_parallel threads at once.
A fork branch can be refused. It counts against max_concurrent_flow_executions; at
capacity it is not started, its caller is not told, and branch_refused is logged.
A flow can bound its own executions. max_concurrent_executions: caps how many executions of
one flow run at once on one engine; the smaller of it and max_concurrent_flow_executions binds.
Over it, a call is refused with 503 or resource_exhausted and the error flow concurrency limit reached, a cron tick is not run, and a queued message waits in its queue with no attempt counted.
The limit is per engine. See A flow’s own limit.
A caller cannot stop a running execution. A caller that disconnects, times out, or is cut off
by a gateway does not stop the flow: it keeps running and performs the effect of every step it
runs. Apart from a step that fails, two things end an execution between its steps, the execution
ceiling and a lost claim; neither interrupts a step that has started. 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
What stops a started execution.
Nothing in a flow is transactional. A flow that fails at step 4 has already performed the
effects of steps 1 to 3, and a later delivery that runs it again repeats them. Under dedup_key:
with dedup_on_failure: absorb, a later delivery of the same key does not run while the key is
remembered: /run replays the recorded failure, and the queue acknowledges the message.
Queue delivery is at-least-once and only first delivery is ordered. A rescheduled message returns after messages submitted later, and there is no priority ordering within a queue. Build for redelivery; see Queues, retries and the DLQ.
Takeover of schedules and queues is bounded, not instant, and misses no message but some ticks. A surviving engine takes the scheduler seat and each queue’s worker within 10 seconds of a process dying, or the NFS lease plus 10 seconds of a host dying; cron ticks inside the gap are not replayed. See Losing an engine.
There is no push export. Metrics are pulled on --metrics-port; the log has exactly
two destinations, both local: the nexus-log.db database and the daily JSON Lines files. Nothing
is sent to syslog, OTLP, statsd, a webhook, fluentd or Kafka. See
Logging and audit.
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.
Masking stops at the log. It applies to JSON object keys when a payload is recorded in the log or in a trace. A body written to a queue is kept as plaintext JSON; its checksum is integrity, not a cipher. See What stays on disk from a queued message.
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 credential inside a message does not authenticate. There is no WS-Security: the gate decides
before a body exists, so a <wsse:UsernameToken> is never read as a credential. See
Authentication.
The WSDL the platform serves is untyped. It is document/literal, and every message is typed
xsd:anyType, so a client that generates typed stubs gets no useful types from it. See
Serving a WSDL.
There is no bit-exact replay of a past delivery. Yesterday’s traffic cannot be replayed over today’s artifact as a form of regression testing.
A ## Fault: section that succeeds acknowledges a queued message. The handler’s answer ends the
delivery in success, so the worker acks it: no retry, no backoff and no dead letter. A message you
meant to park for a human is gone, and the log row reads fault_handled rather than succeeded. If
you want both a handler and a retry, the handler has to fail deliberately. See
What you must do to be safe under redelivery.
A replayed answer is the recorded status and little else. It carries the body and its content type
only with dedup_memoize_response: true, and never the headers the flow wrote: the only headers
added are X-Nexus-Idempotent-Replay and X-Nexus-First-Correlation-Id. And when a duplicate re-runs
a flow that failed before, the effects that already happened run again. See
Deduplication.
The gRPC door does not stream and does not see a flow published after it started. A streaming
method answers unimplemented; the routing table is built at start-up, so a flow deployed afterwards
is routed only after a restart; and a ## Fault: handler cannot deliver its body, because a unary
call under a non-OK status carries no message. See
What this door does not do.
Tenants share one installation. They partition their flows, keys, log, audit and queue files, but run on the same engines, in the same state directory and under the same retention setting. The rate ceiling belongs to a credential, not to a tenant, so a tenant with many keys is sized key by key. See Rate limiting.