Tips > Reliability & Performance

Implement Dead Letter Queues for Failed Items That Need Manual Review

Some failures cannot be automatically retried or recovered.

TipAdvanced3 min read

Some failures cannot be automatically retried or recovered. For these, route failed items to a dead letter queue (DLQ) -- a Google Sheet, database table, or dedicated queue where failed items wait for manual review and reprocessing. This prevents data loss and gives your team a clear remediation process.

Real-world example: A data migration workflow processes 10,000 records. 47 fail due to data quality issues (missing required fields, invalid formats). Rather than losing these records, route them to a DLQ sheet for the data team to fix and reprocess.

// Code node: "Prepare DLQ Entry"
// This runs on the failure branch after the "Split Successes/Failures" node

const failedItems = $input.all();

return failedItems.map(item => ({
  json: {
    // Identification
    dlq_id: `DLQ-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
    created_at: new Date().toISOString(),

    // Source information
    workflow_name: $workflow.name,
    execution_id: $execution.id,
    source_node: 'Enrich Company Data',

    // Original data (for reprocessing)
    original_payload: JSON.stringify(item.json),

    // Error details
    error_message: item.json.error?.message || 'Unknown error',
    error_code: item.json.error?.statusCode || null,

    // Triage fields
    status: 'pending',         // pending | investigating | resolved | ignored
    retryable: item.json.retryable || false,
    assigned_to: null,
    resolution_notes: null
  }
}));
```text
Write to a Google Sheet (Dead Letter Queue):

```yaml
Google Sheets node configuration:
  Operation: Append
  Sheet: "Dead Letter Queue"
  Columns:
    A: dlq_id
    B: created_at
    C: workflow_name
    D: execution_id
    E: source_node
    F: original_payload
    G: error_message
    H: error_code
    I: status
    J: retryable
    K: assigned_to
    L: resolution_notes
```text
Build a reprocessing workflow:

```yaml

# Separate workflow: "DLQ Reprocessor"

# Triggered manually or on a schedule

[Manual Trigger or Schedule]
  --> [Google Sheets: Read rows where status = 'pending' AND retryable = true]
  --> [Code: Parse original_payload back to JSON]
  --> [Original processing logic (copied or via Execute Workflow)]
  --> [IF: Success?]
      True:  [Google Sheets: Update status to 'resolved']
      False: [Google Sheets: Update status to 'investigating', increment retry_count]
```text
```sql
-- Postgres DLQ alternative (better for high volume)
CREATE TABLE dead_letter_queue (
  id SERIAL PRIMARY KEY,
  dlq_id TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  workflow_name TEXT NOT NULL,
  execution_id TEXT,
  source_node TEXT,
  original_payload JSONB NOT NULL,
  error_message TEXT,
  error_code INTEGER,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'investigating', 'resolved', 'ignored')),
  retryable BOOLEAN DEFAULT false,
  retry_count INTEGER DEFAULT 0,
  assigned_to TEXT,
  resolved_at TIMESTAMPTZ,
  resolution_notes TEXT
);

CREATE INDEX idx_dlq_status ON dead_letter_queue(status);
CREATE INDEX idx_dlq_created ON dead_letter_queue(created_at);
```text
No data is lost. Every failed item is accounted for and has a clear path to resolution, whether automatic retry or manual intervention.

**Related:** [Use "Pin Data" to Freeze Node Output](../testing-and-debugging/01-use-pin-data-to-freeze-node-output.md) | [Break Large Workflows into Sub-Workflows](../workflow-architecture/01-break-large-workflows-into-sub-workflows.md)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.