← Webhook errors

Webhook timeout

A timeout means the provider stopped waiting for a 2xx before your handler finished. It is not the same as a 5xx you returned.

What this symptom means

A webhook timeout is the provider giving up before a complete HTTP response. GitHub labels this timed out. Other dashboards say “timeout” or “no response.” Your process may still be running after they disconnect.

This is not a 500 you returned (they got a status line) and not a 502 from a refused port (they got a relay error quickly). Timeouts are slow handlers, blocked outbound calls, or a local debugger paused on a breakpoint.

GitHub documents that it waits 10 seconds for a 2xx, then treats the delivery as failed. See Troubleshooting webhooks — Timed out. Stripe’s docs require a quick 2xx and do not publish a single timeout figure we can cite — do not copy 10/20/30s from unofficial posts.

Common causes

  • Doing the full business workflow (email, ERP, Stripe API retrieve) before res.status(200)
  • Waiting on localhost services that are themselves hung
  • A debugger breakpoint in the webhook route
  • Cold start of a local bundler on the first POST
  • DNS or TLS stalls before Mercur — then Mercur history stays empty (not received)

Verify the incoming request

If Mercur history has a row with a long duration and a 2xx/4xx/5xx, the provider may still have timed out if they cut the connection earlier — but you at least have the payload.

If GitHub Recent Deliveries says timed out and Mercur has no row, the POST never completed to the relay (wrong URL, or they never left GitHub).

Guest inspector answers 204 immediately without running your app. A provider timeout while the URL is the inspector usually means the request never reached Mercur, not that Mercur was slow.

Time a local handler with curl -s -o /dev/null -w '%{time_total}\n' -X POST localhost:<port>/.... If that is already over GitHub’s 10 seconds, a tunnel will not save you.

Provider and framework notes

GitHub: 10 seconds, then timed out, then retries. Respond 2xx, then queue work. Official troubleshooting page above.

Stripe: Return a 2xx quickly, verify signature, persist, process async. No timeout number on that page — do not invent one.

Telegram: long work in the webhook process delays getUpdates-style handling for later updates. Acknowledge the POST, then call Open-Meteo (or anything slow) after the response.

Express/Next: await in the handler holds the HTTP response. Use a queue (or setImmediate / a worker) after you have written the event id. FastAPI: do not await slow I/O before returning 2xx — FastAPI webhook localhost. Next.js first-request compile after next dev can also blow GitHub’s 10 seconds — Next.js webhook localhost.

Debugging checklist

  1. Read the provider’s own timeout docs; do not assume Stripe equals GitHub.
  2. Confirm whether Mercur logged the request.
  3. Time the local handler with curl.
  4. Move slow work off the request thread; return 2xx after verify + persist.
  5. Replay (below) to confirm the new handler stays under the provider’s limit.

Confirm with the webhook inspector

Use the inspector to prove the provider can connect. Guest: 204 without running your code, idle TTL 1 day, cap 256 KB. Timeouts against the inspector are almost never “Mercur was slow.”

Inspect the request Connect localhost

Connect localhost

Timeouts that matter are measured against your handler. Connect the agent, use Log and Proxy, and watch duration in console history plus your process CPU.

Forwarding needs a connected agent — the macOS app or the npm package @mercur_dev/cli.

Mercur Retry itself has a ~30s client timeout on the console fetch — that is not a provider SLA. Keep the handler well under documented provider limits.

Replay the same event

Retry re-posts the stored payload so you can measure the fixed handler without waiting for GitHub/Stripe. Needs proxy mode, agent, complete body. If the handler still runs a 15-second query, GitHub will time out again.

Guest log-only mode cannot replay and cannot show handler duration.

Related