◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-05-31Advanced

Isolating Poison Messages in a Claude Async Pipeline: A Dead-Letter Queue Implementation Note

How one broken input can stall an entire batch — and how to isolate these 'poison messages' with a Cloudflare Queues dead-letter queue. Covers classifying Claude API failures and safe redrive, all from production experience.

Claude API128Async ProcessingCloudflare QueuesDead-Letter QueueProduction23

✦ Premium Article

I once tried to tag a fresh batch of wallpaper metadata by sending 800 image records to Claude in one go. Around record 300 the pipeline froze. Digging through the logs, a single malformed JSON record had crashed the consumer with an exception, and every healthy record behind it got swept into a retry traffic jam. The culprit was one row where a past export bug had left raw binary inside the description field.

This pattern — one broken input dragging down all the healthy ones — is known in the messaging world as a poison message. It stays invisible while you loop over the API synchronously, but the moment you move to an async queue in production, you will hit it at least once. This article is an implementation note on isolating it with a dead-letter queue (DLQ), drawn from what I actually ran into in my wallpaper app backend and the content pipelines behind my four sites.

How a Poison Message Kills the Whole Pipeline

The basic shape of an async pipeline is simple: push jobs onto a queue, and a consumer pulls them one at a time to call the Claude API. It is elegant while everything works — the problem is what happens when the consumer fails.

Most queues use at-least-once delivery: a failed message is not acked, and it gets redelivered after some delay. That is great for transient network blips and 429 rate limits. But when the message itself is broken and fails no matter how many times you process it, that same message gets redelivered forever.

In my first implementation, this redelivery swept up the rest of the work in three ways. First, the one broken record sat at the head of the batch, and because I called retryAll(), the nine healthy records in that batch got resent along with it. Second, every resend burned Claude API tokens, so one poison message quietly inflated my bill. Third, with no retry cap, the poison message lived in the queue forever, eating throughput during its visibility timeout.

The essence of handling poison messages is to separate "failures that heal" from "failures that don't," and physically eject the latter from the healthy flow.

First, Separate Healing Failures from Permanent Ones

Retrying every failure uniformly amplifies poison messages. For a pipeline calling the Claude API, it helps to sort errors into three buckets.

Transient and worth retrying: 429 rate limits, 529 overloaded, 500/503, network timeouts. These heal with time, so exponential backoff is worth it.

Input-caused and permanent (poison): 400 invalid_request (the request body is broken), message deserialization failures, structured outputs that fail schema validation, and inputs too large to ever fit within max_tokens. These should go straight to the DLQ.

Then there is a gray zone: a response truncated with stop_reason: max_tokens, or a refusal. Retrying rarely changes the outcome, but reshaping the input might fix it — so a capped number of retries followed by a DLQ handoff fits best.

Closing this decision inside a single function keeps the consumer clean.

// classify.ts — sort a failure into "retry" or "poison"
import Anthropic from "@anthropic-ai/sdk";
 
export type Verdict = "retry" | "poison";
 
export function classifyError(err: unknown): Verdict {
  // The SDK's APIError carries a status code
  if (err instanceof Anthropic.APIError) {
    const s = err.status;
    if (s === 429 || s === 529 || s === 500 || s === 503) return "retry";
    if (s === 400 || s === 422) return "poison"; // the input is broken
    if (s === 401 || s === 403) return "retry";  // key/IP issues; fixable in config
  }
  // Network-class errors (abort / ETIMEDOUT) are transient
  const name = (err as Error)?.name ?? "";
  if (name === "AbortError" || /timeout|ECONNRESET|ETIMEDOUT/i.test(String(err))) {
    return "retry";
  }
  // JSON.parse failures and schema violations are poison
  if (err instanceof SyntaxError) return "poison";
  // Unknown errors lean to the safe side (retry); the retry cap drops them to DLQ
  return "retry";
}

The key is to not treat the unknown as "poison by default." Unknown errors lean toward retry and naturally fall into the DLQ via the max_retries cap described below. This avoids the accident of throwing away a transient failure you never anticipated by mislabeling it poison.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A decision function that mechanically separates transient failures (retry) from poison messages (isolate) using stop_reason and error types
✦An isolation pipeline built with Cloudflare Queues max_retries and dead_letter_queue so one broken item never stalls the whole batch — with Before/After code
✦A redrive workflow that classifies dead-lettered messages by cause and uses idempotency keys to prevent double-billing on reprocessing
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-05-04
Claude API on Bun in Production: Migration Decisions and Implementation Patterns That Actually Survive Real Traffic
A practical guide to running Claude API services on Bun in production. Covers migration triggers from Node.js, built-in SQLite/WebSocket usage, streaming optimization, and the pitfalls that only surface after deployment — with working code and measured numbers.
⬡ API & SDK2026-04-30
Building a Production Multilingual Translation SaaS with Claude API — Glossaries, Style, and Domain Adaptation in Practice
A practical, code-first design guide for running a translation SaaS on Claude API: glossaries, style guides, domain adaptation, quality gates, and cost controls that survive real production traffic.
⬡ API & SDK2026-04-25
Claude API × Convex: Reactive AI Apps — Data Flow, Streaming, and Agent Patterns
How to combine Convex's reactive database with the Claude API to build chat and agent applications that hold up in production. Covers schema design, the Action/Mutation/Query boundary, streaming, tool-call state, and the cold-start pitfalls nobody warns you about.