Skip to main content

Troubleshooting: message stuck in queued

A message that stays at queued has not been sent to a provider yet — it is parked before the provider handoff. That owning split is deliberate: this page owns the queued lane end-to-end — the cause table for the holds that give that state, the unstick path, and the escalation path. The sister page message parked before sending owns the three other pre-send holds — scheduled, pending, and quiet-hours — and defers a row clearly at queued back here. A row that already passed the queue — submitted_no_receipt, no delivery receipt — belongs to the message sent but no delivery receipt page. This page also carries the status decoder — the status ladder and what each status means for diagnosis — because queued is the centre of the ladder.

How message statuses fit together

Every message moves through a defined set of statuses, and each transition is gated — a message cannot jump from queued straight to delivered, and a terminal status like read or failed cannot move backward. The happy path for an immediate send is:
For a scheduled send, the message sits at scheduled until its send_at time arrives, then joins the queue:
Statuses like accepted (the synchronous pre-queue handoff), test_sent (sandbox mode), and cancelled (a scheduled message you cancelled before it sent) exist alongside the main path. Status updates are ordered, so each row can only move forward along an allowed transition — a stale carrier callback cannot drag a settled message back to queued.

Where to check the status

  • Delivery Log: open Messages → Tools → Delivery log and search the message ID (msg_…), the provider reference, or the recipient. Filter by the Status filter to isolate stuck rows — for example /messages/delivery-log?status=queued&direction=outbound shows every queued outbound message. See the Delivery Log guide.
  • Per-channel workspace: SMS, WhatsApp, and Email channels each show outbound history with the same status values.
  • API: call GET /api/v1/messages with a status=queued filter, or fetch the row by ID. The status field tells you where the row is; the response also tells you whether the row carries a send_at (scheduled), or a provider reference once it reaches sent.

The queued debug ladder

Work this ladder top to bottom when a row sits at queued longer than you expect — it ends at either a tenant-owned fix or a support bundle, without leaving this page:
  1. Isolate the lane. Filter the Delivery Log to status=queued&direction=outbound so you are looking at exactly the rows this page owns, not the parked rows the sister page covers.
  2. Confirm over the API. GET /api/v1/messages?status=queued returns the same lane as JSON; GET /api/v1/messages/:id gives the authoritative row — status, send_at, channel, sender pool, and campaign.
  3. Match the row to the cause table. Each field from step 2 maps to a row in the cause table: a future send_at is the scheduled hold, a campaign points at frequency caps and quiet hours, an exhausted pool points at sender capacity.
  4. Fix the cause, then retry once. A retry against an uncorrected hold re-queues the same row — see What not to do.
  5. Not a hold you own? Escalate with the bundle. If no tenant-owned cause matches and the lane is stuck past a few minutes, open a ticket with your tenant ID and one msg_… ID — see When to escalate for where both live.

Common causes and how to unstick them

Work through these in order — they cover the overwhelming majority of queued-for-longer-than-expected rows.
Split by state, one owner per state. Rows parked at scheduled, pending, or a quiet-hours hold belong to the sister page message parked before sending — it owns those holds as their own states. Rows parked at queued stay here — this page owns that lane. A row that moved on to submitted_no_receipt left the queue entirely — see message sent but no delivery receipt.

Diagnosis checklist

  1. Get the message ID. Open the Delivery Log or your channel workspace and copy the msg_… ID from the row.
  2. Read the message detail. Open the full message detail page — it shows the send channel, the sender pool, the scheduled time if one exists, and the campaign it belongs to. Each of those fields maps to a row in the cause table above.
  3. Check the campaign. If the message came from a campaign, open that campaign’s settings: frequency caps, quiet hours, and sender pool are configured there.
  4. Check tenant-level compliance settings. Quiet-hours and sender-registration gates are organization-wide. Confirm the hold is intentional before you clear it.
  5. Query the API if the dashboard shows conflicting data. GET /api/v1/messages/:id returns the authoritative status, send_at, channel, and any provider reference.

What not to do

Do not spam POST /api/v1/messages/:id/retry while the cause is unknown. A retry against a quiet-hours hold, a pending sender approval, or a capped growth window just moves the same blocked message through the same gate again. Fix the cause first, then retry. Also avoid re-sending a fresh copy of the message to an end user while the original row is still queued — once the hold clears, both copies go out, and your recipient receives the same content twice.

DLR recovery and webhook DLQ replay do not unstick queued

Two recovery tools sit one stage later on the ladder and are easy to mis-apply to a stuck queue row:
  • DLR recovery (Recover a failed or late DLR) retraces a delivery receipt for a row that already reached sent — the provider accepted the message and the carrier outcome went missing. A queued row never reached the provider, so there is no receipt to recover.
  • Webhook DLQ replay (DLQ and recovery) re-delivers events Orbit already emitted after your endpoint exhausted the attempt budget. A row parked before the provider handoff has not emitted its send events yet, so nothing for it sits in the DLQ.
If the row reads queued, the levers are the ladder and the cause table above. Reach for DLR recovery only once the row reads sent or submitted_no_receipt, and for DLQ replay only when your handler missed events that did fire.

Rate limits and quotas that hold the queue

When sends return 429 while rows sit at queued, the error code tells you whether the queue is draining or parked: The distinction that decides your next move: a per-second throughput 429 means dispatch ran ahead of what one sender may carry per second — the queued rows under it are draining, not stuck, and a blind retry only re-joins the same queue. A period quota does not clear on backoff at all: the row parks until the cap changes or the window resets. Raise the cap or wait the cycle; if the reset passes and the lane has not moved, that is your escalation trigger — bundle it with the tenant ID and message ID from When to escalate. The full 429 taxonomy and the backoff pattern live in Rate limits and cool-downs.

When to escalate

If you worked through the checklist, the cause is not a hold you control, and messages are still stuck for more than a few minutes, open a support ticket and include both of these so we can trace the row without a back-and-forth:
  • Your tenant ID (shown in the dashboard under Settings → Organization, and returned by the GET /api/v1/me response as organizationId).
  • The message ID of one stuck row (msg_…).
For a stuck email send, also include your warm-up position — the currentDay and todayCap values from GET /api/v1/email/warmup-status — so support can tell a deliberate ramp hold from a real fault on the first reply. That pair lets support pull the exact queue entry and the exact send-gate evaluation without you having to re-describe the symptom.

Status decoder

Every status value the Delivery Log filter and the GET /api/v1/messages response can return, and what it means for diagnosis: If a row is sitting on any of the blocked-in-queue statuses (queued, scheduled, pending, accepted, sending) for longer than you expect, the cause table and checklist above will get you to the hold faster than a blind retry.

See also