Skip to content

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_ms reaches 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.