Back to Blog

Try n8n free for 10 days — no charge until day 11 on select plans

Or skip the trial and start from $4/mo today

n8npostgresqlwebhookserror handlingtutorial

Building a Webhook Dead Letter Queue in n8n with Postgres Node v2.4

n8nautomation TeamAugust 11, 2026

When running mission-critical automation pipelines on n8n hosting, handling unexpected webhook processing failures without losing incoming payload data requires a dedicated Dead Letter Queue (DLQ). Standard HTTP webhooks follow a fire-and-forget execution pattern. If your downstream services experience temporary outages, rate limits, or database connection drops, incoming payloads are discarded permanently unless caught instantly. By building an isolated failure handler using the Postgres Node v2.4, you ensure every failed event is logged, categorized, and made ready for automated re-execution.

Why Standard Webhook Handling Fails at Scale

Webhooks from providers like Stripe, Shopify, or GitHub require near-instant HTTP 200 responses. If an n8n workflow executes long-running sub-tasks directly inside the primary webhook execution thread, network congestion or slow third-party API endpoints can exceed the caller's timeout threshold (typically 5 to 10 seconds). When a webhook client times out, it marks the attempt as failed and may stop sending subsequent events entirely.

In high-throughput environments, simple retry loops within the same workflow execution do not scale. If an upstream database goes offline for 15 minutes, retrying immediately inside a Wait node consumes system memory, locks database connections, and creates memory pressure that can crash worker threads. A robust architecture separates payload ingestion from payload processing.

  • In-flight Execution Timeout: Synchronous workflows that take too long to resolve get terminated by ingress proxies like Nginx or Traefik.
  • Transient Third-Party Rate Limits: Outbound HTTP Request nodes encountering 429 status codes will fail if retries are not throttled across a delay schedule.
  • Payload Schema Changes: Upstream API field updates can throw unexpected JSON parse errors in Code nodes, causing immediate workflow halts.
  • Loss of Unhandled Payload Data: Default error workflows capture the error execution object, but re-running hundreds of failed executions manually through the n8n UI becomes unmanageable.

Tip: Always configure the Webhook node to return an immediate 200 OK response using the "Respond Immediately" response mode before triggering asynchronous processing nodes.

Designing the Postgres DLQ Schema for n8n

To record and reprocess failed webhook events reliably, you need a resilient database table schema. The schema must store the raw JSON payload, header metadata, error stack trace, processing status, and retry counts. Using PostgreSQL JSONB data types allows schema-agnostic storage, meaning the same DLQ table can process webhooks from multiple distinct sources.

Connect to your PostgreSQL database using an administrative client or psql CLI, and execute the following DDL statement to initialize your DLQ table structure:

CREATE TABLE IF NOT EXISTS webhook_dlq (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    source_system VARCHAR(100) NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    headers JSONB NOT NULL,
    payload JSONB NOT NULL,
    error_message TEXT,
    retry_count INT DEFAULT 0,
    max_retries INT DEFAULT 5,
    status VARCHAR(20) DEFAULT 'PENDING',
    next_retry_at TIMESTAMPTZ DEFAULT NOW(),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_dlq_status_retry ON webhook_dlq (status, next_retry_at) 
WHERE status IN ('PENDING', 'FAILED');

This table design uses conditional indexing on the status and next_retry_at columns. When n8n queries the database for failed jobs ready for reprocessing, PostgreSQL performs an efficient index scan rather than a full table scan over millions of historical archived records.

Configuring the Ingestion Workflow with Webhook Node and Postgres Node v2.4

The primary webhook ingestion workflow acts as a fast-path ingestion layer. Its sole responsibility is validating basic request signatures, assigning a tracking correlation ID, and writing the payload into Postgres if the immediate processing attempt encounters an unexpected error.

  1. Add a Webhook Node v1.1 as the workflow trigger.
    • Set HTTP Method to POST.
    • Set Path to events/ingest.
    • Set Response Mode to When Last Node Finishes (or Immediately for asynchronous patterns).
  2. Connect an Error Trigger Node alongside your primary workflow steps. When any node in the main execution branch fails, execution jumps directly to this trigger branch.
  3. Attach a Postgres Node v2.4 directly to the Error Trigger Node.
    • Set Resource to Database Object.
    • Set Operation to Insert.
    • Set Table to webhook_dlq.

In the Postgres Node v2.4 configuration panel, map the fields dynamically using n8n expressions. Set the column parameters as follows:

  • source_system: ={{ $json.headers["x-vendor-id"] || 'stripe' }}
  • event_type: ={{ $json.body.type || 'unknown_event' }}
  • headers: ={{ JSON.stringify($json.headers) }}
  • payload: ={{ JSON.stringify($json.body) }}
  • error_message: ={{ $execution.error.message || 'Unknown processing error' }}
  • status: PENDING

Implementing Exponential Retry and Failure Capture via Code Node v2

Inserting failed payloads directly into a database is only half the solution. A true DLQ requires intelligent exponential backoff calculation to prevent overwhelming downstream services when they come back online. The Code Node v2 in n8n allows you to programmatically set variable delay intervals based on current retry counts.

When a worker job fails during reprocessing, execute the following JavaScript code inside a Code Node v2 to compute the precise timestamp for the next retry attempt using standard exponential backoff with full jitter:

const items = $input.all();
const baseDelaySeconds = 30;
const maxJitterSeconds = 10;

return items.map(item => {
  const currentRetries = item.json.retry_count || 0;
  const maxRetries = item.json.max_retries || 5;
  
  if (currentRetries >= maxRetries) {
    return {
      json: {
        ...item.json,
        status: 'DEAD_LETTER',
        next_retry_at: null,
        should_retry: false
      }
    };
  }
  
  // Exponential backoff: base * (2 ^ attempt)
  const exponentialFactor = Math.pow(2, currentRetries);
  const calculatedDelay = baseDelaySeconds * exponentialFactor;
  const jitter = Math.floor(Math.random() * maxJitterSeconds);
  const totalDelaySeconds = calculatedDelay + jitter;
  
  const nextRetryDate = new Date();
  nextRetryDate.setSeconds(nextRetryDate.getSeconds() + totalDelaySeconds);
  
  return {
    json: {
      ...item.json,
      retry_count: currentRetries + 1,
      status: 'PENDING',
      next_retry_at: nextRetryDate.toISOString(),
      should_retry: true
    }
  };
});

If the calculated retry_count exceeds max_retries, the job status updates to DEAD_LETTER. This permanently removes the job from active automated queues, preventing poison pill payloads from triggering infinite processing loops in your database infrastructure.

Note: Ensure your n8n instance clock is synchronized via NTP. Discrepancies between system time and database server time will cause `next_retry_at` comparisons to delay executions unexpectedly.

Building the Automated Reprocessing Sub-Workflow

To process items stored in the DLQ automatically, create a secondary scheduled n8n workflow that polls PostgreSQL at fixed intervals. This decoupling keeps polling overhead completely isolated from incoming webhook traffic.

  1. Add a Schedule Trigger Node set to execute every 2 minutes.
  2. Connect a Postgres Node v2.4 using the `Execute Query` operation to pull pending payloads:
    UPDATE webhook_dlq
    SET status = 'PROCESSING',
        updated_at = NOW()
    WHERE id IN (
        SELECT id 
        FROM webhook_dlq 
        WHERE status = 'PENDING' 
          AND next_retry_at <= NOW()
        ORDER BY next_retry_at ASC
        FOR UPDATE SKIP LOCKED
        LIMIT 50
    )
    RETURNING id, source_system, event_type, headers, payload, retry_count, max_retries;
  3. Attach a Loop Over Items Node v1.0 to iterate through returned failure batches safely without exceeding memory limits.
  4. Pass the `payload` and `headers` into an **Execute Workflow Node** containing the business processing logic.
  5. Branch execution using an **If Node v2.2** based on the workflow result:
    • On Success: Route to a Postgres Node v2.4 updating status to RESOLVED.
    • On Failure: Route to the Code Node v2 (exponential backoff script above) and update Postgres status back to PENDING or DEAD_LETTER.

Using FOR UPDATE SKIP LOCKED in PostgreSQL is vital here. If you scale your n8n instance to multi-worker Queue Mode or run multiple polling workers simultaneously, `SKIP LOCKED` prevents race conditions by guaranteeing that no two n8n workers pull the same failed webhook row concurrently.

Managing Webhook Throughput: Self Hosted n8n vs Managed Hosting

When running complex retry logic, vector storage queries, or high-volume Webhook Dead Letter Queues, infrastructure choices determine reliability. Many developers start by learning how to install n8n manually on basic cloud VPS servers. However, operating a **self hosted n8n** server for production workloads introduces operational complexities that quickly compound.

Configuring PostgreSQL connection pools, monitoring node processes, managing Docker volume mounts, setting up SSL certificates, and tuning Redis queues require ongoing server administration. If your host server runs out of disk space due to uncollected Docker log files or growing database tables, your n8n workflow execution engine will fail silently, missing incoming webhooks entirely.

  • Database Resource Contention: High webhook throughput on single-instance Docker stacks causes severe I/O contention between n8n's internal execution logging and your external PostgreSQL DLQ table.
  • Maintenance and Upgrades: Upgrading n8n core packages on self-managed infrastructure risks downtime if database migrations fail mid-update.
  • Uptime and Backups: Manual deployments require custom backup scripts to ensure your DLQ records and active workflow configurations are preserved safely off-site.

If you want to skip manual server administration and infrastructure maintenance, using n8n managed hosting from n8nautomation.cloud provides a fully optimized runtime out of the box. Starting at just $4/month, it represents the standard for low cost n8n hosting and offers dedicated resources, automatic daily backups, instant subdomains, custom domain flexibility, and direct dashboard access to live server logs.

Furthermore, if you are moving off a fragile self-hosted instance, n8nautomation.cloud provides a built-in migration tool that safely transfers all your workflows within seconds by simply providing host URLs and API keys. This ensures your production workflows and Dead Letter Queue systems remain operational with zero maintenance overhead.

Ready to automate with n8n?

Get affordable managed n8n hosting with 24/7 support.