Good Webhooks

Performance

Measured publish and delivery throughput for a fixed profile on one machine, how it was measured, and what the numbers do not say.

These numbers come from pnpm benchmark in the repository: a fixed profile, local receivers, one PostgreSQL instance. They show what the library costs on its own. They are not a capacity guarantee for your receivers, network, or database.

Profile

SettingValue
Endpoints20, each in its own scope
Events250 per endpoint, 5,000 in total
Deliveries1 per event, 5,000 in total
Workers2 processes, concurrency: 32 each
Per-endpoint in-flight cap8
ReceiverLocal HTTP, verifies the signature, 5 ms delay, 204
Pool20 connections per process
Publish20 endpoints in parallel, sequential per endpoint, each publish in its own transaction

Results

Measured on 12 October 2026. Three consecutive runs; the table shows the middle run.

MeasurementResult
Publish rate3,350 events/s
Drain rate, 5,000 queued deliveries1,550 deliveries/s
Time to drain the backlog3.2 s
Attempt latency, claim to recorded outcomep50 17 ms, p95 23 ms, p99 28 ms
Time in queue, publish to successp50 2.5 s, p95 3.2 s, max 3.4 s
Attempts5,000 succeeded, 0 retried, 0 abandoned

Time in queue is dominated by backlog depth: every event was published before the first worker started, so the median event waited for half the backlog. With workers already running, time in queue for a single event is close to the attempt latency plus the poll interval.

Environment
MachineApple M3 Pro, 12 cores, 18 GB
Node.js24.21
PostgreSQL16.15 in Docker on the same machine

What the numbers do not say

  • Receiver time dominates in production. The receiver here answers in 5 ms. A receiver that takes 300 ms makes delivery throughput a function of concurrency and maxInFlight, not of the library.
  • One machine. Workers, receivers, PostgreSQL, and the benchmark share CPU. Separate hosts change both directions: less contention, more network latency.
  • No failures. The profile has no retries, timeouts, or lease recoveries. Failure behavior and the operations rehearsal cover those paths; they are slower by design.
  • Small history. The tables start empty. Delivery claims use partial indexes on status, so a large succeeded history should not slow claims, but this profile does not prove it.

Run it yourself

export TEST_DATABASE_URL='postgres://postgres:webhooks_dev_only@127.0.0.1:55439/webhooks'
pnpm benchmark

The script creates a unique schema, runs the profile, prints a JSON report, and drops the schema. It fails if any delivery does not succeed exactly once. Change scripts/benchmark/config.mjs to model your own receiver delay or concurrency, and compare runs on the same machine only.

On this page