API & Automation·8 min read·

Building Resilient Webhook Delivery Engines with Idempotency Keys

Architect a fault-tolerant webhook delivery system featuring HMAC-SHA256 signature verification, idempotency keys, and jittered exponential backoff.

NB

Netbay Engineering

Netbay Engineering

On this page

Webhooks are the backbone of modern asynchronous API architectures. Payment gateways, Git providers, and SaaS platforms rely on webhooks to notify external systems of domain events in real time. However, building a production-grade webhook dispatcher is notoriously tricky: consumer endpoints crash, DNS lookups time out, network blips cause duplicate deliveries, and naive retry loops can accidentally DDoS customer infrastructure.

A production-ready webhook delivery engine must enforce three core guarantees: cryptographic tamper resistance (HMAC-SHA256), end-to-end idempotency, and jittered exponential backoff retries with dead-letter queue (DLQ) triage.

Why At-Least-Once Delivery Requires Idempotency

In distributed systems, achieving exactly-once delivery across unreliable networks is mathematically impossible under the Two Generals Problem. If an HTTP POST request is sent to a consumer and the connection abruptly drops while waiting for the HTTP 200 response, the sender cannot discern whether the consumer processed the event or failed before processing.

The only safe strategy is **at-least-once delivery combined with receiver-side idempotency**. By attaching a globally unique Idempotency-Key header to every webhook dispatch, consumers can record processed keys in an atomic in-memory cache or relational database table, immediately acknowledging duplicate requests without re-executing business side effects.

Cryptographic Signatures and Replay Protection

Every webhook payload should include an HMAC-SHA256 signature computed over the raw request body and a timestamp header. This allows consumers to verify payload authenticity and reject replay attacks older than five minutes:

typescript
// webhook-signer.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function signWebhookPayload(payload: string, secret: string, timestamp: number): string {
  const signaturePayload = "t=" + timestamp + "," + payload;
  const signature = createHmac("sha256", secret).update(signaturePayload).digest("hex");
  return "t=" + timestamp + ",v1=" + signature;
}

export function verifyWebhookSignature(payload: string, header: string, secret: string): boolean {
  const parts = header.split(",");
  const timestampPart = parts.find((p) => p.startsWith("t="));
  const signaturePart = parts.find((p) => p.startsWith("v1="));

  if (!timestampPart || !signaturePart) return false;

  const timestamp = parseInt(timestampPart.slice(2), 10);
  const signature = signaturePart.slice(3);

  // Reject replays older than 300 seconds (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > 300) return false;

  const expectedSignature = createHmac("sha256", secret)
    .update("t=" + timestamp + "," + payload)
    .digest("hex");

  return timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expectedSignature, "hex"));
}

Consumers use timingSafeEqual to avoid timing side-channel vulnerabilities during cryptographic verification.

Expand
Webhook Delivery Infrastructure
Asynchronous Webhook Pipeline Architecture

Enforcing Idempotency with Valkey / Redis

Network timeouts often occur after the recipient receives the payload but before the HTTP response reaches the sender. Re-transmissions are therefore inevitable. Send an Idempotency-Key header on every delivery attempt so consumers can safely ignore duplicate executions:

sql
-- PostgreSQL schema for tracking webhook events and delivery attempts
CREATE TABLE webhook_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    destination_url TEXT NOT NULL,
    event_type VARCHAR(64) NOT NULL,
    payload JSONB NOT NULL,
    idempotency_key VARCHAR(128) NOT NULL UNIQUE,
    status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
    attempt_count INT NOT NULL DEFAULT 0,
    next_retry_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE webhook_delivery_logs (
    id BIGSERIAL PRIMARY KEY,
    event_id UUID REFERENCES webhook_events(id) ON DELETE CASCADE,
    attempt INT NOT NULL,
    http_status INT,
    response_body TEXT,
    error_message TEXT,
    duration_ms INT NOT NULL,
    delivered_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_webhook_events_retry ON webhook_events(status, next_retry_at)
WHERE status = 'PENDING';

Implementing Jittered Exponential Backoff

When a customer endpoint fails with HTTP 500 or times out, retrying immediately exacerbates their outage. Implement full jitter exponential backoff:

python
# backoff.py
import random
import time

def calculate_backoff(attempt: int, base_delay: float = 2.0, max_delay: float = 3600.0) -> float:
    """
    Computes exponential backoff with full jitter to avoid thundering herds.
    Delay = UniformRandom(0, Min(max_delay, base_delay * 2 ** attempt))
    """
    exponential = base_delay * (2 ** attempt)
    ceiling = min(max_delay, exponential)
    return random.uniform(0.5 * ceiling, ceiling)

# Example schedule for 5 attempts:
for attempt in range(5):
    delay = calculate_backoff(attempt)
    print(f"Attempt {attempt + 1}: Next retry in {delay:.2f} seconds")

If an event reaches its maximum retry threshold (typically 5 to 10 attempts over 24 hours), mark its status as DEAD_LETTER so developers can inspect and replay it via an administrative API.

Resilient Webhook Dispatcher & Retry Architecture Event Ingestion - Domain Event fired - Gen idempotency key - Sign HMAC-SHA256 - Store in PostgreSQL 16 Status: PENDING Dispatch Engine - POST to consumer URL - Timeout cap: 5 seconds - If 2xx: Status COMPLETED - If 5xx/Timeout: Calc backoff - Full jitter scheduling DLQ & Telemetry - Max attempts reached - Status: DEAD_LETTER - Admin replay endpoint - SSRF IP range validation Audit log preserved Jittered backoff prevents cascading overload during external network outages
REST API Design, Idempotency, and HTTP Protocols Explained
YouTube

Preventing Server-Side Request Forgery (SSRF)

Always validate destination webhook URLs before initiating HTTP connections. Block loopback ranges (127.0.0.0/8), private RFC 1918 subnets (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and link-local cloud metadata endpoints (169.254.169.254). Perform DNS resolution immediately prior to socket creation to avoid DNS rebinding vulnerabilities.

Deploy robust webhook workers and backend APIs on Netbay Linux VPS in under 60 seconds with High-Speed SSD in Lucknow DC01 — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance