HookRelay

Reliable webhook delivery with durable state, signed requests, bounded retries, and dead-letter recovery. Every attempt stays inspectable.

Type
Webhook delivery system
Role
Backend & full-stack engineering
Scope
Durable delivery · local reference implementation

Tech stack

  • TypeScript
  • Node.js
  • Fastify
  • PostgreSQL
  • Drizzle ORM
  • Redis
  • BullMQ
  • React
  • Vite
  • Zod
  • Vitest
  • Docker

Outbound webhooks need a reliability layer between accepting an event and a receiver actually processing it. Network failures, timeouts, transient HTTP errors, and duplicate requests are normal parts of that path.

HookRelay is a production-style local reference implementation of that layer. It accepts events idempotently, commits delivery state to PostgreSQL, schedules asynchronous work through Redis and BullMQ, signs outgoing requests, and records every attempt.

HookRelay dashboard showing delivery states and operational metrics
A PostgreSQL-backed operations view of deliveries, attempts, and recovery.

The Fastify API validates endpoint and event input with Zod. Event acceptance creates an event and its initial delivery in one PostgreSQL transaction, before queue scheduling is attempted.

BullMQ carries the durable deliveryId. The worker reloads canonical event and endpoint data from PostgreSQL, starts a persisted attempt, signs and sends the request, classifies the response, and writes the outcome back to the database.

HookRelay architecture showing the API, PostgreSQL, Redis, worker, receiver, and dashboard
Queue infrastructure coordinates execution without becoming the source of delivery truth.

The dashboard reads deliveries, attempt timelines, retry scheduling, latency, failures, and replay lineage through the API. Metrics are calculated from PostgreSQL state rather than inferred from transient queue contents.

POST /events requires an Idempotency-Key. The first request creates one event and one initial delivery. Repeating the same key with the same endpoint, event, and payload returns the existing records. Reusing the key for different input returns 409 idempotency_conflict.

PostgreSQL uniqueness makes concurrent identical requests converge. An in-memory lock would not provide the same database-backed guarantee.

Durable acceptance comes before queue scheduling. If Redis scheduling fails, the API returns 503 with durableAccepted: true, asking the client to retry the same request with the same idempotency key.

That boundary is part of the design, not a hidden promise of exactly-once execution. A receiver can process a request before a worker crashes while recording success; a later retry may deliver the same logical event again.

Retries follow a fixed 5s / 15s / 45s / 120s schedule, for a maximum of five attempts. A five-second request timeout bounds the network operation. Only failures classified as transient consume that retry budget.

OutcomeClassificationAction
HTTP 2xxSuccessMark delivered
HTTP 408, 429, or 5xxRetryableRetry if budget remains
Network error or timeoutRetryableRetry if budget remains
Other HTTP responseTerminalMove directly to dead letter
Retryable failure on attempt fiveExhaustedMove to dead letter
HookRelay attempt timeline with two HTTP 500 responses followed by HTTP 200
The deterministic recovery scenario succeeds on attempt three; the preceding failures remain visible.

Only dead-letter deliveries can be replayed. Replay has its own idempotency key and creates a new linked delivery with a fresh retry budget. The source is never reset: its terminal status and original attempt history stay intact.

HookRelay dead-letter delivery and linked replay
Recovery creates a new delivery linked by replayedFromDeliveryId, preserving the original evidence.
ChoiceWhyCost
PostgreSQL as the source of truthDelivery state, attempts, metrics, and replay lineage remain durable.The database participates in the critical execution path.
BullMQ jobs carry only delivery IDsWorkers reload canonical records instead of trusting stale queued payloads.Each execution needs database reads.
Fixed retry delays and a five-attempt budgetRecovery is bounded and easy to reproduce.This version does not implement adaptive retry policies.
New delivery for manual replayPreserve the failed history and give the replay an independent budget.Operations must understand linked delivery lineage.
Durable acceptance before schedulingAvoid losing or duplicating durable rows when scheduling fails.The dual-write gap needs client retry; no outbox is implemented.
At-least-once semanticsAcknowledge the crash window between remote processing and local persistence.Receivers must deduplicate events.

Every request is authenticated with HMAC-SHA256 over timestamp.rawBody. The included receiver example validates timestamp freshness and compares signatures in constant time. Authentication and multi-tenancy for the platform itself remain outside the current version.

The committed local benchmark used one API process, one BullMQ worker, one PostgreSQL instance, one Redis instance, and an immediate local HTTP receiver.

MeasurementLocal result
Measured runs3 × 500 deliveries
Terminal success1,500 / 1,500 · 100%
Delivery throughput71.0–78.6 deliveries / second
Mean delivery throughput76.0 deliveries / second
Ingestion throughput426.7–599.6 events / second
Client-observed ingestion p9548.5–70.0 ms

Two correctness tests each issued 25 concurrent requests. Identical ingestion converged to one event and one initial delivery. Identical replay requests converged to one replay delivery, while the source dead letter remained unchanged.

The deterministic receiver also exercises immediate success, 500 → 500 → 200 recovery, and exhaustion after five failures through the real API, queue, worker, signing, and database paths.

Read the committed benchmark evidence ↗

The implemented local scope includes the API, durable data model, queue worker, request signing, bounded retries, dead-letter recovery, idempotent replay, dashboard, deterministic demo, tests, and benchmark evidence.

The next reliability extension would address the documented database / queue dual-write gap with an outbox or reconciliation mechanism. That is outside the current implementation and is not represented as shipped.

Other deliberate non-goals are platform authentication and multi-tenancy, secret encryption or rotation, horizontal worker-capacity validation, multi-region delivery, and high-availability guarantees. Keeping those boundaries visible makes the local evidence easier to interpret.

Does HookRelay guarantee exactly-once delivery?

No. Delivery is at least once. A worker can crash after a receiver processes a request but before local success is recorded, so consumers should deduplicate by event ID.

Which responses are retried?

Network errors, five-second timeouts, HTTP 408, HTTP 429, and 5xx responses are retryable. Other non-2xx responses go directly to dead letter. The maximum is five attempts.

What happens if queue scheduling fails?

The event and delivery have already been persisted. The API returns 503 with durableAccepted: true and asks the client to retry the same request with the same idempotency key.

Does replay overwrite the failed delivery?

No. Replay creates a new delivery linked to the source, with its own attempt history and retry budget. The original remains a dead letter.

How can a receiver verify a webhook?

Use the supplied event identity, timestamp, and HMAC-SHA256 signature over timestamp.rawBody. The repository includes a verifier demonstrating freshness checks and constant-time comparison.

Is there a hosted production service?

The README describes a local reference implementation with a deterministic receiver, operations dashboard, tests, and bounded benchmarks. It does not claim a hosted production service or high-availability guarantees.

Project details and measurements from the repository README ↗.Checked 23 October 2026

LET’S CONNECT

LET’S TALK ABOUTWHAT’S NEXT.

I’m open to contracts, internships, and remote engineering roles.

STRAIGHT TO MY INBOX