Skip to main content

Runtime Contract

This guide defines the runtime guarantees that Excalibur provides during message execution.

Execution Modes​

Dispatch runs in one of two modes:

  • Local mode: message is handled in-process.
  • Transport mode: message is routed through configured transport adapters.

Routing is resolved before pipeline execution.

Pipeline Contract​

Execution order is:

  1. Dispatcher receives message + caller cancellation token.
  2. Route decision is resolved.
  3. Middleware executes in registration order.
  4. Final handler/transport execution occurs.
  5. Result is returned to caller.

Cancellation Contract​

  • Cancellation tokens are part of the API contract and must propagate end-to-end.
  • A DispatchAsync call from within a handler inherits correlation and cancellation budget.
  • Canceled requests must not continue work in the same request pipeline.

Context Contract​

When context is materialized, these fields are expected:

  • CorrelationId
  • CausationId
  • message identity/type metadata

Lean local paths can defer full context creation, but correlation/causation semantics must remain correct whenever context is requested.

Retry and Poison Contract​

  • Retry behavior is policy/profile based.
  • Exhausted retries route to poison/dead-letter handling when configured.
  • Dead-letter records must retain actionable failure metadata.

Payload Size Contract​

  • Oversized messages are rejected at the boundary — measured before the body is deserialized — so a single large payload cannot exhaust memory, strand a batch, or poison a queue. Ingress fails closed: the message is rejected, never truncated and never silently passed.

  • Outbox publish enforces a maximum serialized payload size (OutboxDeliveryOptions.MaxPayloadBytes, default 4 MiB); an over-limit message is rejected before staging rather than written and retried forever.

  • Every transport receive/subscribe path enforces its own maximum inbound payload size before deserialization. An over-limit delivery is never handed to your handler and never deserialized, it is logged (a …PayloadTooLarge event), and the rest of the batch keeps processing — no poison-loop, no stranded batch:

    SurfaceConfigure viaDefault limit
    Outbox publishOutboxDeliveryOptions.MaxPayloadBytes4 MiB
    AWS SQSIAwsSqsTransportBuilder.UseMaxPayloadBytes(int?)256 KiB (SQS message ceiling)
    Azure Service BusAzureServiceBusProcessorOptions.MaxPayloadBytes256 KiB
    Google Pub/SubGooglePubSubOptions.Subscriber.MaxPayloadBytes10 MiB
    gRPCGrpcTransportOptions.MaxPayloadBytes4 MiB
    KafkaKafkaConsumerTuningOptions.MaxPayloadBytes4 MiB
    RabbitMQRabbitMqConsumptionOptions.MaxPayloadBytes4 MiB (nacked requeue: false)
  • How the over-limit message is settled depends on whether you have given it somewhere to go, and this is the part worth configuring deliberately:

    Your configurationWhat happens to the over-limit message
    A dead-letter destination is configured (for example an SQS redrive policy to a dead-letter queue, or a Pub/Sub dead-letter topic)It is preserved and routed there by the broker's own mechanism, so you keep a copy to investigate. On SQS this means the message is left to become visible again so its receive count advances — deleting it would destroy the copy the dead-letter queue exists to capture.
    No dead-letter destination is configuredIt is dropped. Retaining a message that can never be processed and has nowhere to go would redeliver it forever and stall the queue, so liveness is preferred. If you need these messages kept, configure a dead-letter destination — that is the switch.

    A message that is merely large is not the same as one that is corrupt: raising MaxPayloadBytes, or setting it to null, makes an otherwise-valid large payload processable rather than dead-lettered.

  • The default is deliberately bounded (never unbounded) so the guard is never inert for a consumer who never configures one. Set MaxPayloadBytes to null to opt out (unbounded) for larger legitimate payloads, or raise it to a specific ceiling. A non-positive value is rejected at startup (ValidateOnStart) rather than silently bricking delivery.

Release Enforcement​

This contract is release-blocked by:

  • transport conformance tests,
  • release-blocking CI test governance gate,
  • architecture/governance validation.

See Also​