Event Consumer Dead-Letter Capture#
Goal: Configure durable-queue limits and inspect or replay messages captured in the Event Consumer Service’s dead-letter queue.
When to use: This applies to deployments that use the Event Consumer Service’s durable queues. It governs what happens to a durable-queue message once it has been redelivered too many times or has aged out, routing it to a dedicated dead-letter queue (DLQ) for later inspection.
This page covers the deployment-facing concerns for this feature: the broker prerequisite, the topology the service declares for you, the durable-queue bounds policy the Helm chart applies (including the idle-queue TTL that reaps unattended durable queues), how the dead-letter queue’s own bounds differ, the one-time migration of existing durable queues, the operator-facing dead-letter management RPC and the permission that gates it, and the recovery path for inspecting and recovering captured dead-letters.
Use the section for your task:
First deployment: Broker Requirements, Durable-Queue Bounds Policy and Durable Queue IDs and Permissions.
Existing deployment: Rollout Sequence: Existing Durable Queues.
Recover captured messages: Dead-Letter Management RPC: Operator Runbook.
Restrict operator access: Granting the DLQ-Management Permission.
Note
Read the broker requirements and queue configuration before installing Event Consumer Service. Use Notifications Services Configuration for the installation commands. The recovery runbook requires a running broker and Event Consumer Service.
Broker Requirements#
RabbitMQ 4.x with quorum queues is a hard prerequisite for this feature. RabbitMQ 4.1.5 or newer is recommended. Validate that your broker is on the 4.x line before relying on dead-letter capture.
The management HTTP API must be reachable from the cluster, and a credential that may set policies on the notifications vhost must exist. The chart’s hook Job uses it to apply the Durable-Queue Bounds Policy.
Note
RabbitMQ 4.0.5 through 4.1.4 have a policy-repair bug when the delivery limit
is disabled. The chart rejects negative deliveryLimit values. See the
RabbitMQ 4.1.5 release notes.
This guide’s downloadable broker values use image 4.1.3-debian-12-r1.
If selecting a newer broker, use a container compatible with the RabbitMQ chart
and validate the deployment before upgrading. The redelivery guidance below
describes RabbitMQ 4.1; RabbitMQ 4.3 changes which requeues increment the delivery
count. See RabbitMQ poison-message handling
before upgrading across those versions.
Durable / quorum deployment prerequisites#
The durable origin queues and the DLQ are quorum queues, so the broker must be deployed to actually persist and replicate them:
Persistent storage must be enabled on RabbitMQ (
persistence.enabled: true). Quorum durable queues persist to disk; with persistence disabled, the queues and their messages are lost on pod restart, defeating durability.For high availability, run a multi-node cluster (for example
replicaCount: 3together withclustering.enabled: true) so the quorum durable queues replicate across nodes via Raft. A single node yields quorum semantics but only one replica — there is no redundancy. Single-node works for non-production but provides no HA.
A concise values.yaml fragment for a production-grade broker:
persistence:
enabled: true
replicaCount: 3
clustering:
enabled: true
Scaling an existing single-node broker to a cluster#
Raising replicaCount on a broker that is already running adds nodes, but it
does not re-replicate quorum queues that already exist — a quorum queue keeps
the Raft membership it had when it was declared.
If no quorum queues have been declared yet, there is nothing more to do. Scale up first; with RabbitMQ’s default group size, queues declared after all three nodes have joined start with three replicas. Enable the cluster before turning on durable-queue workloads.
Only if durable queues already exist (
event-queue:*quorum queues and/or the DLQ, which the Event Consumer Service declares at startup): after the new nodes have joined, run a one-time grow per new node so the existing queues gain a replica there —rabbitmq-queues grow "rabbit@<new-node-name>" all
using the node names shown by
rabbitmqctl cluster_status. Queues declared after the scale-up need no migration in either case.
App-Declared DLX/DLQ Topology#
After the broker prerequisites and policy are configured, the Event Consumer Service declares the capture topology over AMQP at startup (and the declarations are reconnect-safe — they are re-asserted on reconnect). The service declares:
The dead-letter exchange (DLX) — a durable direct exchange.
The dead-letter queue (DLQ) — a durable quorum queue. The DLQ is self-bounded by its own
x-message-ttlandx-max-lengthwithx-overflow: drop-head, and it has no dead-letter-exchange of its own, so there is no dead-letter cycle (dead-letters cannot themselves be dead-lettered into a loop).The DLX → DLQ binding — the binding on the dead-letter routing key that routes messages from the DLX to the DLQ.
Each durable origin queue is declared with the queue arguments
x-queue-type: quorum, x-dead-letter-exchange, and
x-dead-letter-routing-key only — pointing at the DLX so that dead-lettered
messages are routed to the DLX (and from there to the DLQ via the binding above).
The DLX and DLQ names, the dead-letter routing key, and the DLQ’s self-bound defaults are configurable via ``OMNI_EVENTS_``-prefixed settings on the Event Consumer Service:
OMNI_EVENTS_DEAD_LETTER_EXCHANGE_NAME— the DLX name (events.dlx).OMNI_EVENTS_DEAD_LETTER_QUEUE_NAME— the DLQ name (events.dlq).OMNI_EVENTS_DEAD_LETTER_ROUTING_KEY— the routing key (dead-letter) used for the DLX → DLQ binding and the origin queues’x-dead-letter-routing-key.OMNI_EVENTS_DEAD_LETTER_QUEUE_MESSAGE_TTL_MS— the DLQ’s ownx-message-ttl(1209600000milliseconds, or 14 days).OMNI_EVENTS_DEAD_LETTER_QUEUE_MAX_LENGTH— the DLQ’s ownx-max-length(1000000messages).
These settings are container environment variables. The chart does not expose
DLQ topology or retention settings as Helm values; changing them requires adding
the corresponding environment variables to your managed Deployment manifests.
Adding them to event-services-env alone does not inject them into the service.
Keep the same topology settings across all service replicas, and avoid naming the
DLQ with the event-queue: prefix used by the durable-queue policy.
Warning
These are retention limits, not a retention guarantee. Expired DLQ messages and messages evicted when it reaches its maximum length are permanently discarded. Monitor its depth and message age and triage before either limit is reached. RabbitMQ’s delivery limit also applies to repeated DLQ requeues; this chart does not set a separate DLQ delivery-limit policy.
The chart’s default drop-head policy uses RabbitMQ’s at-most-once
dead-letter transfer. A broker failure or unavailable DLQ can lose a message
during transfer. Publisher-confirmed replay from the DLQ is a separate operation;
it does not make the initial capture lossless. See
RabbitMQ dead-lettering guarantees.
Retuning the dead-letter queue’s bounds#
The dead-letter queue is bounded by a different mechanism than the durable
origin queues. OMNI_EVENTS_DEAD_LETTER_QUEUE_MESSAGE_TTL_MS and
OMNI_EVENTS_DEAD_LETTER_QUEUE_MAX_LENGTH are queue arguments applied when
the service first declares the queue, not policy values, and RabbitMQ queue
arguments are immutable once a queue exists. Changing either on a running
deployment does not retune the live queue. The chart’s policyHook does not
cover the DLQ either — its ^event-queue: pattern does not match the
dead-letter queue name, and that name is a service setting the chart does not
template.
On each (re)connect with mismatched arguments, the service logs an ERROR
beginning:
Dead-letter queue argument mismatch
The line names the desired-but-unapplied values and the remedies below, then the
service completes startup against the existing queue — dead-letter capture
keeps working and pods do not crash-loop on a configuration change. Search for that
prefix to detect the drift. The line’s trailing Broker error: … text names the
offending argument, and it need not be one of these two settings: if it names
something else (x-queue-type, say, on a queue that is still a classic queue or
was provisioned by another tool), no setting change or policy can clear it.
Two ways to act on a mismatch:
To lower a bound on the live queue, apply a broker policy scoped to the dead-letter queue (default
events.dlq, or whateverOMNI_EVENTS_DEAD_LETTER_QUEUE_NAMEis set to). Where an argument and a policy both set a numeric bound, RabbitMQ takes the lower of the two — so a policy can tighten a bound but can never raise one above the value the queue was created with. Omit ``overflow`` from that policy: unlike the numeric keys it is policy-wins on quorum queues, so including it overrides the queue’s owndrop-headargument and silently changes how the DLQ behaves when full.# Self-hosted broker, with node access: rabbitmqctl set_policy -p notifications dlq-limits '^events\.dlq$' \ '{"max-length":500000}' --apply-to quorum_queues # Any broker, including a managed one: the management HTTP API. # URL-encode the vhost in the path ("/" is %2F). curl -u "$RABBITMQ_USER:$RABBITMQ_PASSWORD" -X PUT \ -H 'content-type: application/json' \ -d '{"pattern":"^events\\.dlq$","definition":{"max-length":500000},"apply-to":"quorum_queues","priority":0}' \ "https://<broker-host>/api/policies/<vhost-urlencoded>/dlq-limits"
Keep every dead-letter bound in this one policy: at most one regular policy applies to a queue, so a second one matching the same queue displaces the first rather than merging with it.
To raise a bound — or to clear a mismatch on an argument these settings do not control — the queue has to be recreated. Schedule a maintenance window and quiesce publishing and durable consumers. Replay messages that must be retained, and discard only messages you intend to delete, using Dead-Letter Management RPC: Operator Runbook. Confirm the DLQ has no ready or unacknowledged messages, then stop the Event Consumer Service pods before deleting the queue. Keep producers paused and monitor for further dead-letters: origin-queue TTL expiry can still send messages to the DLQ while clients are stopped. A missing DLQ can lose those transfers. Restart the service with the new settings, verify the DLQ and binding, and then resume workloads. Deleting a nonempty queue permanently loses its contents.
# Self-hosted broker, with node access: rabbitmqctl delete_queue -p notifications events.dlq # Any broker, including a managed one: the management HTTP API. curl -u "$RABBITMQ_USER:$RABBITMQ_PASSWORD" -X DELETE \ "https://<broker-host>/api/queues/<vhost-urlencoded>/events.dlq"
Note
Both routes are also available on the Policies and Queues pages of the RabbitMQ
Management UI. On a managed broker you have no node access and therefore no
rabbitmqctl — the management API and the UI are the only routes.
Durable-Queue Bounds Policy#
The per-origin-queue bounds — delivery-limit, message-ttl, max-length,
overflow, and the idle-queue TTL expires — are not set as queue
arguments by the app. They are applied as a RabbitMQ broker policy so that they
can be retuned without recreating queues.
The Event Consumer Service Helm chart owns this policy. A
post-install,post-upgrade hook Job applies it through the broker’s management
HTTP API on install and on every helm upgrade. It checks the existing policy
and writes only when it differs. Keep policy configuration in the Event Consumer
Service values file; remove any older RabbitMQ init script that writes the same
policy.
If the hook cannot apply the policy, the Helm release fails. This is a post-install/post-upgrade hook: service pods may already be running when it fails. Resolve the failure and verify the effective broker policy before allowing durable workloads to use the deployment.
The policy it applies:
Policy name:
event-queue-limits(policyHook.policy.name)Vhost:
policyHook.managementApi.vhost—notificationsin this guide’s deploymentPattern:
^event-queue:(matches durable origin queues namedevent-queue:<queue_id>when combined withapply-to: quorum_queues)Definition (defaults; these are starting points):
delivery-limit: 10(policyHook.policy.definition.deliveryLimit) — must be>= 0; the chart refuses a negative value.message-ttl: 86400000(24 hours) —…definition.messageTtlMsmax-length: 100000—…definition.maxLengthoverflow: drop-head—…definition.overflowexpires: 86400000(24 hours) —…definition.expiresMs, the idle-queue TTL. See Durable-Queue Idle TTL (Auto-Reap) below before changing it.consumer-timeoutis omitted by default —…definition.consumerTimeoutMs. See Bounding a Hung Durable Consumer before setting it.
Configuring and retuning#
Retuning is a Helm values change followed by helm upgrade — the hook Job
re-applies the policy with the new definition, and existing queues pick it up
without being recreated.
The chart has no defaults for reaching the broker, because a policy applied to the wrong broker or the wrong vhost succeeds silently and bounds nothing. It refuses to render unless all three of these are set:
policyHook.managementApi.baseUrl— scheme, host and port, no trailing path. Never assume:15672; a managed broker commonly serves the management API over HTTPS on 443.policyHook.managementApi.vhost— must be the same vhost the service consumes from. The Job parses the vhost out of the service’s own AMQP URI and fails the release on a mismatch.policyHook.managementApi.existingSecret.name— a Secret in the release namespace withusername/passwordkeys. The credential needs thepolicymakertag plus access to the vhost (administratoralso works, but is not required). Only the hook mounts this management Secret. Use a separate management user to keep policy privileges out of the service’s AMQP credential.
See Notifications Services Configuration for the step-by-step version of this, including creating the credential Secret.
# event-consumer-values.yaml
policyHook:
managementApi:
baseUrl: http://rabbitmq:15672
vhost: notifications
existingSecret:
name: rabbitmq-mgmt-credentials
policy:
definition:
deliveryLimit: 10
messageTtlMs: 86400000
maxLength: 100000
overflow: drop-head
expiresMs: 86400000
The chart fixes the pattern at ^event-queue: and the scope at
quorum_queues. Keep that scope if managing the policy yourself: non-durable
queues share the name prefix but must not receive these durable-queue bounds.
The default DLQ name events.dlq does not match the pattern.
Note
At most one regular policy applies to a queue (highest priority wins; equal
priorities resolve nondeterministically). A second regular policy matching
^event-queue: displaces this one rather than merging with it, so keep
every bound for these queues in this single policy. Do not add a supplementary
policy of your own alongside it.
Note
An unchanged policy is not re-applied. The Job reads the existing policy first and skips the write when it already matches, because applying a policy resets the idle clock on every queue it matches — an unconditional write on a frequent deploy cadence would keep abandoned queues alive indefinitely.
Managing the policy yourself#
policyHook.enabled: false is the escape hatch for operators on restricted
brokers who manage policies themselves. The three values above are then not
required and the chart applies nothing. What that costs depends on whether the
policy had ever been applied, and the two cases are not the same:
Never applied (a fresh cluster, or a broker where you removed the policy yourself): in the absence of other broker policies, durable queues have no maximum length, message TTL or idle-queue TTL. RabbitMQ 4.1’s built-in delivery limit is
20. Apply an equivalent policy before starting durable workloads.Disabled after it had been applied: nothing in the chart deletes the policy. The last-applied
event-queue-limitspolicy stays live on the broker and keeps enforcing, including reaping idle durable queues, frozen at whatever values it last carried. Disabling the hook stops the chart from updating the policy; it does not remove it. To actually get rid of it, delete it yourself —rabbitmqctl clear_policy -p notifications event-queue-limits, or the Policies page in the management UI.
Durable-Queue Idle TTL (Auto-Reap)#
The expires key in the policy above governs the lifetime of a durable
queue, and it is the one bound with a client-visible consequence. A durable
queue ends in one of two ways: a DeleteDurableQueue call, or the broker
reaping it after it has gone expiresMs — 24 hours by default —
without an attached consumer.
For service clients, an attached consumer keeps the queue alive. Publishing to the queue and re-declaring it do not. The idle clock starts when the consumer disconnects or cancels.
Messages in a reaped queue are discarded, not dead-lettered. They do not appear in the DLQ. Messages published after the last consumer detached can still be younger than
messageTtlMswhen the queue expires. Matching the two TTLs does not prevent this loss.A returning client fails loudly, not silently. Consuming from a reaped queue returns
NOT_FOUNDon gRPC /404on REST rather than an empty stream.Recovery is to recreate the queue. Call
CreateDurableQueueagain with the storedqueue_idand the storedfilter_groups. Creation is ensure-exists: a missing queue is created with those filters; an existing queue is returned unchanged, including its existing filters. Clients must persist the queue ID and its filter groups together. Events published while the queue was absent cannot be recovered through that queue.
Tuning the window. A deployment whose clients are legitimately idle for longer
than the window — a batch job that runs weekly, for example — can raise
expiresMs rather than have those clients take the recreate-on-return path.
Setting expiresMs to null omits the key entirely and disables reaping,
restoring the old “exists until deleted” behavior, at the cost of abandoned queues
accumulating on the broker forever. 0 is not valid — the broker rejects it.
Prefer expiresMs at or above messageTtlMs, while accounting for the
remaining loss window described above.
Note
Expect the first auto-reap one full idle window after the deploy that first
applies the policy. Applying it — on first install, and on any upgrade that
changes a policy value — resets the idle clock on every queue it matches,
including queues that have already been abandoned for weeks. Those queues are not
reaped at the next tick; they are reaped expiresMs after the apply. A cluster
with a long-standing backlog of abandoned durable queues therefore stays at its
current queue count for a full window (24 h at the default) before the count
starts falling. That is expected, not a broken hook.
Tuning Redelivery and Acknowledgement Limits#
The policy values are starting points. Size deliveryLimit above routine
redelivery from client reconnects, failed processing and channel closures.
On RabbitMQ 4.1 these requeues spend the same delivery budget. Durable consumers
prefetch 20 messages by default (rabbitmq.durable_consumer_prefetch_count);
a smaller prefetch reduces the number requeued when a consumer disconnects.
Bounding a Hung Durable Consumer#
The acknowledgement timeout is configured through
policyHook.policy.definition.consumerTimeoutMs. Its default is null,
which omits the policy key and leaves the broker’s timeout in effect (30 minutes
by default on RabbitMQ 4.1). There is no service-level 60-second default.
OMNI_EVENTS_CONSUMER_TIMEOUT_MS has been removed and is ignored.
For example, to request a five-minute acknowledgement timeout:
policyHook:
policy:
definition:
consumerTimeoutMs: 300000
Allow enough time for a whole prefetch batch to be processed:
consumerTimeoutMs > durable_consumer_prefetch_count × worst-case processing time in ms
The chart rejects values below 60000. On RabbitMQ 4.1, values below five
minutes are discouraged and enforcement runs on a one-minute tick. See
RabbitMQ acknowledgement timeouts.
When the broker cancels a consumer, the service ends its stream with an error.
Clients must reconnect; if the queue is missing, they must recreate it with
CreateDurableQueue first. An error after a REST stream has opened is sent in
an SSE error event, not as a changed HTTP response status.
Rollout Sequence: Existing Durable Queues#
This section applies only to existing deployments. Before upgrading, inventory each durable broker queue and save its client’s queue ID and filters. Check that the destination IDs are unique across the deployment: older queues with different identity prefixes could have the same queue ID. Assign distinct replacement IDs where needed, save the old-to-new mapping, and update the corresponding client configurations. Otherwise, ensure-exists creation can return one shared queue with the first client’s filters for multiple clients.
Two changes require attention:
Older releases used
event-queue:<hashed-principal-id>:<queue-id>broker names. The current service usesevent-queue:<queue_id>. Existing queues with the old names are not reachable through the current durable API, even if they are already quorum queues. Changing the service version does not move their messages or bindings.A classic queue at a name the service now needs cannot be converted by redeclaring it. RabbitMQ returns
406 PRECONDITION_FAILEDfor the queue-type mismatch.
Schedule a maintenance window:
Pause event publishers and drain the old queues with the old service and clients. Resolve captured dead-letters whose
origin_queuestill names an old queue; the management RPC cannot redirect them to a new name.Stop the old consumers and deploy the new service with the management Secret and
policyHookconfiguration. Verify the policy hook succeeds.For any classic queue at a required new name, confirm it is empty and delete it before recreating it. Do not delete queues with messages you need.
Call
CreateDurableQueuewith each destinationqueue_idand its savedfilter_groups. A consume reconnect alone does not create a durable queue.Verify there is a distinct new quorum queue for each intended destination, with the expected bindings. Connect clients using their mapped IDs, then resume publishers. Retire old empty queues after validation.
Events published while the required queue or bindings are absent are not recoverable through that queue. A rollback also requires planning for the queue names understood by the older service.
Durable Queue IDs and Permissions#
Queue IDs must match ^[A-Za-z0-9._-]{1,200}$ and share one deployment-wide
namespace. Use a UUID or a naming convention unique to the client application.
CreateDurableQueue with an existing ID returns that queue unchanged; it does
not replace its filters. Save both the ID and filters for recovery.
The consume-durable-queues, create-durable-queues (also used for updates),
and delete-durable-queues permissions are deployment-wide. They do not enforce
ownership of individual queues. Grant them to appropriate service accounts and
avoid reusing another application’s queue ID.
Recovery Path#
The supported, primary way to inspect and recover captured dead-letters is the
ManageDeadLetteredEvents management RPC — see
Dead-Letter Management RPC: Operator Runbook below. It browses the DLQ and lets
an operator REPLAY / DISCARD / LEAVE each message. Replay waits for a publisher
confirm before acknowledging the DLQ copy.
RabbitMQ’s built-in tooling remains available only as a low-level fallback — for example, to inspect the DLQ directly when the management RPC is unavailable, or for broker-level operations the RPC does not expose:
The RabbitMQ Management UI, or
rabbitmqadmin
against the DLQ (the queue named by OMNI_EVENTS_DEAD_LETTER_QUEUE_NAME).
Captured messages remain subject to the DLQ’s retention and redelivery limits. Broker tools can consume or delete messages too; use them with the same care as the management RPC.
Dead-Letter Management RPC: Operator Runbook#
The Event Consumer Service exposes an operator-facing
ManageDeadLetteredEvents RPC on the EventConsumerService for inspecting and
triaging captured dead-letters. It is a bidirectional-streaming gRPC on the
service’s gRPC surface (Helm service.grpc.port, default 50051; the
standalone container default is 50052). There is no REST/SSE parity — this
operation is gRPC-only.
Note
With permissions.enabled: true, the caller must hold
event-consumer-service:inspect-and-replay-all-dlq — see
Granting the DLQ-Management Permission below. A caller lacking it is denied
with PERMISSION_DENIED at stream open, before any dead-lettered event is
streamed.
Open the management stream#
Use a bidirectional gRPC client with the Notifications Consumer API specifications matching your service release. Connect to the configured gRPC endpoint with the operator’s bearer token and open:
nvidia.omniverse.notifications.consumer.v1beta.EventConsumerService/ManageDeadLetteredEvents
Send an empty opening request. In protobuf JSON form:
{}
Keep the request stream open while reading responses and sending actions. The
DLQ is global to the deployment, so the opening request has no queue_id.
Actions must use delivery tags received on this same live stream.
Browse dead-letters#
Inspect each event’s reason and origin_queue before acting. Reasons are
REASON_DELIVERY_LIMIT, REASON_EXPIRED and REASON_MAXLEN. The
delivery_count is broker metadata, not an exact count of processing attempts
on the origin queue. Consult the API specification linked above for all fields.
Use the response’s delivery_tag to select a message. It is valid only within
that live stream; never persist it or reuse it after reconnecting.
Act per message#
Send an action with the message’s delivery_tag:
ACTION_REPLAYrepublishes to the origin queue and removes the DLQ copy after broker confirmation. A disconnect before acknowledgement can produce a duplicate, so downstream processing must tolerate duplicates.ACTION_DISCARDpermanently removes the message.ACTION_LEAVErequeues it. Avoid repeatedly leaving the same message: it can return immediately, and on RabbitMQ 4.1 each requeue consumes its DLQ delivery budget.
For example, after deciding to replay the event whose delivery tag is 42,
send this subsequent request on the same stream, replacing 42 with the
tag you actually received:
{"actions": [{"deliveryTag": "42", "action": "ACTION_REPLAY"}]}
A successful replay produces the following protobuf JSON response:
{"actionOutcome": {"deliveryTag": "42", "outcome": "OUTCOME_REPLAYED"}}
These examples use protobuf JSON field names. Check the action_outcome for
each delivery tag: OUTCOME_REPLAYED confirms publication, not downstream
processing. OUTCOME_ORIGIN_QUEUE_DELETED or OUTCOME_REPLAY_FAILED leaves
the message available subject to DLQ retention and redelivery limits; inspect
detail and resolve the failure before retrying. The API specification lists
all outcome values.
Reason-driven triage#
Read the ``reason`` before acting. A REASON_DELIVERY_LIMIT message that keeps
failing will simply re-dead-letter if blindly replayed — a replay loop. Fix the
root cause first, or discard the message with ACTION_DISCARD if it is no
longer needed. For REASON_EXPIRED or REASON_MAXLEN, resolve the backlog or
capacity problem and confirm that processing historical events is still
appropriate before sending ACTION_REPLAY. Watch the per-action Outcome reports to
confirm publication, then monitor the origin queue and DLQ for repeated failure;
an outcome report does not track what happens after publication.
Origin-queue-deleted behavior#
If an ACTION_REPLAY targets an origin queue that no longer exists, the
RPC emits an explicit OUTCOME_ORIGIN_QUEUE_DELETED outcome report back to the operator
on the stream (keyed to the delivery_tag) and leaves the DLQ copy in place
(LEAVE semantics). Recreating the origin queue restores a replay target. Verify that the queue
still belongs to the intended application before replaying. A queue recreated
through the current API cannot receive a replay addressed to an old
identity-derived broker name.
Replay Scope#
Replay targets the broker queue name recorded in origin_queue; the caller
cannot select a different destination. This is not a tenant-isolation boundary:
queue IDs are deployment-wide, and if another application recreates a deleted ID,
replay targets that new queue. Confirm the destination before replaying.
The DLQ-management permission grants visibility and actions across the entire DLQ. It does not apply per-event permission checks while browsing. Replayed messages are checked against the consuming caller’s configured permissions when consumed normally, provided permission enforcement is enabled.
Granting the DLQ-Management Permission#
Set permissions.enabled: true, configure permissions.endpoint, and select
an authenticated principalSource as described in
Notifications Services Configuration.
Then grant event-consumer-service:inspect-and-replay-all-dlq to the DLQ
operators. With enforcement enabled, a caller lacking it is denied with
PERMISSION_DENIED at stream open.
Configure this permission in your Permissions Service deployment (see Permissions Service Configuration):
Declare the action for the
event-consumer-serviceservice:inspect-and-replay-all-dlq. The fully-qualified action string isevent-consumer-service:inspect-and-replay-all-dlq.Grant the action to the principal(s) that operate the DLQ — bind
event-consumer-service:inspect-and-replay-all-dlqto the operator role or principal.
Note
permissions.enabled defaults to false. In that mode this permission
check is bypassed, including for authenticated callers. The tokenless
getting-started configuration therefore does not protect DLQ management.
Enable authorization before exposing it to users.