Skip to content

Glossary drawing · Reliability & scale

Poison Message

Also called: Poison pill · Poison pill message

Note 1 —A queued message or job that fails its consumer every time it is processed, so it retries forever unless it is detected and quarantined.

Term
Poison Message
Abbreviation
—
Category
Reliability & scale
Level
Practitioner
Reviewed
Sheet no.
GLO-REL-577
Drawn by
InfiQ Glossary

Drawing 1 · Interactive

Walk through a worker that quarantines poison messages

6 / 6
const NEVER_RETRY = new Set([100, 131047, 132000, 132001]);
const MAX_ATTEMPTS = 5;
export async function processJob(job) {
let payload;
try {
payload = JSON.parse(job.data.raw);
} catch (err) {
return quarantine(job, "unparseable_json", { message: err.message });
}
if (job.attemptsMade >= MAX_ATTEMPTS) {
return quarantine(job, "attempt_limit_reached");
}
const res = await sendMessage(payload); // POST /{PHONE_NUMBER_ID}/messages
if (res.ok) return;
const error = res.body.error ?? {};
if (NEVER_RETRY.has(error.code)) {
return quarantine(job, "meta_error_" + error.code, error);
}
throw new Error("transient " + (error.code ?? res.status)); // retry with backoff
}
async function quarantine(job, reason, detail = {}) {
await deadLetters.add("quarantined", {
original: job.data, reason, detail,
attempts: job.attemptsMade, fbtrace_id: detail.fbtrace_id,
});
log.warn({ event: "poison_message", reason, job_id: job.id });
}

Step 6 of 6 · lines 26–32

Quarantine keeps the original payload, the reason and the attempt count, and logs a structured event that alerting can match.

Jump to
A Node.js queue worker, step by step: which failures it quarantines on sight, where the delivery cap catches the rest, and what it records so the message can be fixed and replayed.

worker.js, step 6 of 6, lines 26–32: Quarantine keeps the original payload, the reason and the attempt count, and logs a structured event that alerting can match.

What is a poison message?

A poison message (also called a poison pill) is a message on a message queue or stream that its consumer can never process successfully. Every time a worker picks it up the same thing happens — a parse error, a validation failure, an exception in the handler — and the message goes back on the queue to be tried again. Without a limit it loops forever, burning worker time and filling the logs with the same stack trace.

In a WhatsApp integration the usual suspects are concrete: a webhook payload your parser does not expect (a new message type, a missing field), a send job whose values no longer line up with each message template variable of the approved template, so the Cloud API returns error 132000 every time, a job that points at a media ID which has already expired, or a text body longer than the API accepts. None of these will succeed on attempt two, ten or fifty, because nothing about the input changes between attempts.

The cure has two halves. Detection: count deliveries (ApproximateReceiveCount on SQS, the x-delivery-count header on RabbitMQ quorum queues, an attempts counter in most job libraries) and classify errors in your retry policy so that a permanent failure is recognised immediately. Quarantine: move the message aside, usually to a dead-letter queue, with the reason and the last error attached. The poison message is the problem; the dead-letter queue is where it waits for a human.

Key takeaways

  • A poison message is a payload that fails its consumer on every attempt, so retrying it only wastes capacity and blocks other work.
  • Detect it two ways: a delivery-count cap catches anything stuck, and error classification catches permanent failures on the first try.
  • A WhatsApp send that fails with error 132000 (parameter count mismatch) fails identically on every retry, so quarantine it instead.
  • The poison message is the root cause; the dead-letter queue is only where it gets parked for inspection and replay after a fix.
  • In an ordered stream such as a Kafka partition, one poison record stalls everything behind it until it is moved aside.

Operating sequence

How does a poison message work?

  1. Cap deliveries per message

    Configure a maximum delivery count on the queue (for example a redrive policy with maxReceiveCount: 5) or check the attempt counter in the worker. Anything past the cap is treated as poison, whatever the error.

  2. Classify errors on the first failure

    Some failures are permanent by nature: JSON that does not parse, a schema violation, or Cloud API errors such as 100 (invalid parameter), 132000 (parameter count mismatch) and 132001 (template does not exist). Quarantine these at once instead of spending the remaining attempts.

  3. Quarantine with context

    Move the message to a dead-letter queue together with the reason, the error code and message, the Graph API fbtrace_id, the attempt count and the consumer version, then acknowledge the original so it leaves the main queue.

  4. Alert on arrivals

    Alert when the first message lands in quarantine and when the arrival rate rises. One poison message often signals a deploy or template change that is about to poison many more.

  5. Fix, then replay

    Correct the cause — the parameter mapping, the parser, the data — and replay quarantined messages through the normal path, each carrying its idempotency key, so anything that did succeed earlier is not sent twice.

Implementation

A poison message in code

WhatsAppSendQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 60
RedrivePolicy:
deadLetterTargetArn: !GetAtt WhatsAppSendDLQ.Arn
maxReceiveCount: 5 # the 6th receive moves it to the DLQ
WhatsAppSendDLQ:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14 days, the SQS maximum

A delivery cap at the queue level catches anything the worker fails to classify. Other brokers have equivalents: a delivery limit on RabbitMQ quorum queues, an attempts option in job libraries.

Example: One unknown event that stalled a Kafka partition

A Bengaluru electronics retailer streams inbound WhatsApp webhooks into Kafka, partitioned by the customer's wa_id so each conversation stays in order. One afternoon a customer sends a message type the parser was never written for, and the consumer throws on it every time. Because the consumer retried in place and never committed past the record, partition 7 stopped: about 1,900 messages from other customers on that partition waited 40 minutes, while dashboards showed the other partitions healthy. The fix has two parts. The consumer now validates each event against a schema and, after three failures, publishes the record with its error to a side topic, commits past it and alerts. The parser was updated the next morning and the quarantined event replayed, with no other conversation blocked.

Failure modes

Common mistakes with a poison message

  • Retrying everything the same way

    A blanket 'retry five times' wastes four attempts on errors that cannot succeed and delays the alert. Classify first: retry transient errors, quarantine permanent ones on sight.

  • Dropping instead of quarantining

    Deleting a failing message stops the loop but loses the evidence and the customer notification. Park it with its payload and error so it can be fixed and replayed.

  • Losing the reason

    A dead-letter queue full of bare payloads forces someone to reproduce every failure. Attach the error code and message, fbtrace_id, attempt count and the code version that failed.

  • Blocking an ordered stream

    On a Kafka partition or a FIFO queue grouped by customer, a consumer that retries in place halts every later message. After the cap, move the record to a side topic and commit past it.

  • Replaying without idempotency

    Replaying a batch after a fix can resend messages that partly succeeded earlier. Key each send by a business ID and check it before calling the API.

Specifications

A poison message: key facts and limits

Common delivery caps
3–10 deliveries, set per queue
Never-retry Cloud API errors
100, 132000, 132001 (input or template problems)
SQS delivery counter
ApproximateReceiveCount; redrive via maxReceiveCount
RabbitMQ quorum queues
x-delivery-count header and a delivery limit
Kafka
No built-in redelivery count; a failing record stalls its partition
SQS dead-letter retention
Up to 14 days (1,209,600 seconds)

Design intent

Why does a poison message matter?

One poison message can do outsized damage. On a shared queue it takes a worker slot on every redelivery and floods alerts with identical errors, hiding real incidents. On an ordered stream it blocks every message behind it, so a single malformed webhook can stall all inbound chats on that partition. If each retry calls the Cloud API, the bad job also spends throughput on requests that are certain to fail. Detecting and quarantining poison messages keeps healthy traffic flowing while the broken few wait for a fix.

Recommended practice

Best practices for a poison message

  • Watch quarantine arrivals closely after every deploy and template edit — poison messages cluster around releases.
  • Validate webhook payloads against a schema at the edge, so malformed events are recognised before they reach a queue.
  • Keep the raw payload of quarantined events with phone numbers masked; it is usually enough to reproduce the bug.
  • Build a one-click replay for the dead-letter queue and rehearse it before you need it.
  • Record every quarantine with structured logging — reason, error code, template name — so dashboards can count poison messages by cause.

Poison Message FAQs

What is the difference between a poison message and a dead-letter queue?+
A poison message is the failing payload; the dead-letter queue is the holding area it is moved to. A dead-letter queue also collects messages that expired or ran out of retries for transient reasons, so not everything in it is poison.
How many retries before a message is considered poison?+
Most teams cap at three to ten deliveries. Pair the cap with exponential backoff so the attempts span minutes rather than milliseconds, and treat obviously permanent errors as poison on the very first failure.
Can a WhatsApp webhook event be a poison message?+
Yes. If your handler throws on an event shape it does not expect — a new message type or a missing field — that event fails every time it is processed. Return 200 to Meta quickly, queue the event, and quarantine whatever your parser cannot handle.
Should I retry error 132000 from the Cloud API?+
No. Error 132000 means the parameters you sent do not match the template's variables, so the same request fails every time. Fix the parameter mapping or the template, then replay the quarantined jobs.

See InfiQ pricing

Put Poison Message to work

InfiQ is an official Meta Business Partner. Start a 7-day free trial — no card needed — and see this running on your own number.