CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
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 API115Async Processing2Cloudflare 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-07-07
When Claude API Suddenly Starts Returning 429 in Production — Field Notes on Measuring Rate-Limit Headroom from Headers and Throttling Before You Run Dry
Scrambling to add retries after a 429 is always a step behind. Claude API writes how much you have left into every response header. These are field notes on measuring that headroom continuously and throttling yourself before it runs out.
API & SDK2026-07-01
When Claude API Document Extraction Is Confidently Wrong — Field Notes on Catching Silent Errors with Invariants
In structured extraction from invoices and contracts, the real danger isn't a crash — it's a value that's silently wrong while the schema validates and confidence reads high. Field notes on invariants, two-pass extraction, and tracking field-level error rates.
API & SDK2026-06-23
When the Same Model Has a Different Name Everywhere — Designing a Cross-Provider Model Identity Resolver for Claude
Now that Fable 5 is available on the API, Bedrock, and Vertex at once, the same model carries a different identifier on each. Here is how to untangle hardcoded model strings with a small resolver that maps logical names to physical IDs, carries capability flags, and verifies identifiers at startup.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →