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.

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_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 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.