A deliberately unreliable webhook receiver — the live demo behind Webhook Relay's durable retries.
It fails on purpose so you can watch Webhook Relay refuse to give up: every webhook is retried, with exponential backoff, until it finally lands.
sender ──▶ Webhook Relay ──(durable retries)──▶ tunnel ──▶ flakey-script
├─ ~80% of early attempts → 500
└─ same id, 1h later → 200 (always)
Send it a webhook like:
{ "type": "event", "id": "xx-123", "data": "test" }For each unique id:
- The first time an
idis seen, the receiver remembers when. - While that
idis younger than 1 hour, the receiver returns500about 80% of the time (and200the rest — the occasional lucky early success). - Once that
idis at least 1 hour old, it always returns200. This is the "endpoint has recovered" moment that lets Webhook Relay's retries finally succeed. - Once an
idhas succeeded, every later retry for it is an idempotent200— it is never processed twice.
State is kept on the filesystem (DATA_DIR/state.json), so it survives restarts and you can watch every id converge on delivered over time — both here and in your Webhook Relay dashboard.
A live dashboard is served at http://localhost:3000:
| field | meaning |
|---|---|
| Webhooks | unique ids received |
| Delivered | ids that have succeeded at least once |
| Retrying | ids still failing (will be guaranteed to succeed once 1h old) |
| Total attempts | every delivery attempt, including the failed ones |
You'll need a Webhook Relay account (free) and the relay CLI or the dashboard.
1. Create a bucket (a public endpoint to receive webhooks):
relay bucket create durable-demo2. Create a tunnel that exposes this app's port to Webhook Relay. The destination is the compose service name:
relay tunnel create flakey-demo -d http://flakey-script:3000 -c flexible -r eu
# note the assigned host, e.g. 01abc….webrelay.io3. Point the bucket's output at the tunnel and turn on durable retries (dashboard → bucket → Outputs → add a public destination https://01abc….webrelay.io → Retries → choose a schedule).
4. Create an agent token at https://my.webhookrelay.com/tokens and drop it into .env:
cp .env.example .env
# fill in RELAY_KEY / RELAY_SECRET, set TUNNELS=flakey-demo5. Start everything:
docker compose up -d --build6. Send some webhooks at the bucket's input endpoint and watch them converge:
./send.sh https://<your-bucket>.hooks.webhookrelay.com 20
open http://localhost:3000 # local dashboardNow stop fighting it — turn your laptop off for a while if you like. Webhook Relay treats that as just another outage and keeps the retries queued; when the app comes back (and the ids are old enough) everything lands.
To keep the dashboard alive with a steady stream of traffic converging over time, run the optional producer container. It POSTs a fresh webhook to your bucket endpoint on an interval (default every 30s) and auto-stops after a few days.
# set WEBHOOK_ENDPOINT (and optionally PRODUCER_* vars) in .env, then:
docker compose --profile producer up -d
docker compose logs -f producer| env var | default | description |
|---|---|---|
WEBHOOK_ENDPOINT |
— | bucket input URL to send to (required) |
PRODUCER_INTERVAL_MS |
30000 |
delay between webhooks (30s) |
PRODUCER_BATCH_SIZE |
1 |
webhooks per tick |
PRODUCER_STOP_AFTER_HOURS |
72 |
auto-stop after ~3 days (0 = forever) |
| env var | default | description |
|---|---|---|
PORT |
3000 |
listen port |
DATA_DIR |
/data |
where state.json is written |
FAILURE_RATE |
0.8 |
fraction of early attempts that fail |
RECOVERY_AFTER_MS |
3600000 |
age at which an id is guaranteed to succeed (1h) |
To see a full cycle in seconds instead of an hour, run locally with a short window:
RECOVERY_AFTER_MS=20000 FAILURE_RATE=0.8 npm start| method | path | purpose |
|---|---|---|
POST |
/ |
receive a webhook (the flaky one) |
GET |
/ |
live HTML dashboard |
GET |
/api/state |
JSON summary + per-id state |
GET |
/healthz |
health check |
POST |
/admin/reset |
clear all state |
MIT