Data Pipeline Error Handling: Dead-Letter Queues, Retries and Monitoring

Handling failures in data pipelines: dead-letter queues, failure categories, retries with backoff and jitter, circuit breakers and the metrics to monitor.

Intermediate 3 min read · Updated

Production pipelines fail all the time: network partitions, schemas that drift between producers and consumers, malformed records from buggy upstream systems, external services that rate-limit or time out. A robust pipeline expects this. Its three goals: don't lose data, don't block progress on unrelated records, and surface problems for someone to fix.

Dead-letter queues: don't drop, don't block

When a record can't be processed you can drop it (silent data loss), block the pipeline (one bad record stops everything), or sideline it: put it in a dead-letter queue (DLQ) and keep going. Good records flow to the output; failed ones go to the DLQ with details of why.

successfailed after retriesreplayInput topicProcessorvalidate, transform, writeOutputwarehouse / topicDead-letter queuerecord + error + stage + versionOperatorsinspect, fix, replay
Dead-letter queue: good records continue; failures are retried, then parked with error metadata so the pipeline never stalls on one bad record.

Operators then triage the DLQ: transient failures (timeouts, rate limits) usually succeed when replayed; data bugs (malformed JSON) are fixed upstream and discarded; pipeline bugs mean fixing the code and reprocessing. Enrich every DLQ entry with the original record, error message, timestamp, pipeline version and the failing stage, and tag an error category (validation, serialization, downstream timeout) to prioritize.

Kinds of failure

CategoryExampleUsual fix
Schema validationNull where a value is required; a string where a number is expectedProducer bug or schema drift: fix the source. See data contracts
Business rule violationNegative quantity, future timestamp, unknown user IDEither valid data your pipeline doesn't handle yet, or an upstream bug
Transient downstream failureDatabase timeout, API rate limit, service briefly downRetry with backoff
Serialization / parsingCan't decode JSON or deserialize AvroUsually a producer/consumer schema version mismatch

Retrying the right way

  • Immediate retry is usually wrong: the service timed out because it's overloaded, and instant retries add load and keep it down: a feedback loop of failure.
  • Exponential backoff spaces attempts out (1 s, 2 s, 4 s, 8 s), giving the dependency time to recover.
  • Jitter adds randomness. Without it, 100 workers that failed together retry together (a thundering herd) and fail again; with it, they spread out (1.3 s, 0.9 s, 1.7 s…).
  • Circuit breaker: if a dependency keeps failing (say 50% failures over 10 seconds), stop calling it and fail fast; probe periodically and resume when it recovers. That protects both you and the struggling service. See circuit breakers.
  • Cap retries: without a limit, records retry forever, burning resources and hiding the real problem.
0 s1 s2 s3 s4 s5 s6 s7 s8 sfailretry 1retry 2retry 3→ DLQ + alert
Exponential backoff with jitter: waits roughly double after each failure, with random spread so many workers don't retry in lockstep; after the last attempt the record goes to the DLQ.
retry_delay = base_delay * 2 ** attempt + random(0, base_delay)

A typical policy: try; on failure retry up to 3 times with exponential backoff and jitter; then write to the DLQ, alert, and move on to other records. Network blips recover in 2–3 retries; longer outages (deployments, database failover) may need more attempts or longer backoff. See retries and timeouts.

import json, random, time
from kafka import KafkaConsumer, KafkaProducer

MAX_RETRIES, BASE_DELAY = 3, 1.0
consumer = KafkaConsumer("orders", bootstrap_servers="localhost:9092",
                         group_id="orders-loader", enable_auto_commit=False)
producer = KafkaProducer(bootstrap_servers="localhost:9092")

class Transient(Exception): ...           # timeouts, 429s, connection resets

def process(record: dict):
    if record.get("quantity", 0) < 0:
        raise ValueError("negative quantity")       # permanent: retrying won't help
    write_to_warehouse(record)                       # may raise Transient

for msg in consumer:
    error = None
    try:
        record = json.loads(msg.value)
        for attempt in range(MAX_RETRIES + 1):
            try:
                process(record); error = None; break
            except Transient as e:
                error = e
                time.sleep(BASE_DELAY * 2 ** attempt + random.uniform(0, BASE_DELAY))
    except (ValueError, json.JSONDecodeError) as e:   # bad data: no retries
        error = e
    if error is not None:
        producer.send("orders.dlq", json.dumps({
            "original": msg.value.decode(errors="replace"), "error": repr(error),
            "category": type(error).__name__, "topic": msg.topic, "offset": msg.offset,
            "pipeline_version": "2026.10.1", "failed_at": time.time()}).encode())
    consumer.commit()                                  # progress continues either way

Monitoring: make failures visible

Pipelines have no user staring at them, so failures must surface through monitoring, not complaints:

SignalWhat it tells you
LagHow far behind real time the pipeline is (processing 9:45 events at 10:00 = 15 minutes). Growing lag means throughput dropped or input grew
Error rateShare of records failing. A spike points to a bad deploy, schema change or dependency outage; a slow rise to degrading upstream data
DLQ depthSome DLQ entries are normal; rapid growth means an ongoing problem
Data quality checksOutput counts match inputs (allowing for filtering), values in valid ranges, duplicates at expected rates; catches bugs that produce wrong results without errors. See data quality
End-to-end latencyEvent creation to availability in the serving layer: what users actually experience, even if every stage looks fast

Treat pipelines as production systems: health dashboards, alerts before users notice, on-call rotations with escalation paths, runbooks for common failures, and capacity planning. A silent pipeline failure that corrupts analytics for a week is often worse than a user-facing outage fixed in minutes. See observability.

Where it’s used

Event ingestion

Kafka consumers send unparseable or invalid events to a DLQ topic and keep consuming.

Queue-based workers

SQS and RabbitMQ move messages to a DLQ after a maximum receive count.

Warehouse loads

Rejected rows land in an errors table with the reason, and the load continues.

Third-party API syncs

Backoff with jitter plus a circuit breaker around rate-limited APIs.

In an interview

  • For every pipeline, say what happens to a bad record: retry transient errors with backoff and jitter, then DLQ with metadata, and never block the stream.
  • Separate permanent errors (don't retry) from transient ones.
  • Name the monitoring: lag, error rate, DLQ depth, data quality and end-to-end latency.