Webhook Monitoring: Detect Gaps in Inbound Events
Last verified on Nov 8, 2026.
A webhook is a promise from another system that it will call you when something happens. When that promise breaks, nothing on your side errors. There is no failed request and no exception, only a stretch of time in which events that should have arrived did not. Orders stay unpaid in your database, a shipment never updates and a subscription renewal is missed until a customer writes in.
The silence has several causes: the sender is down or delayed, your endpoint rejected deliveries and the sender gave up, a signature secret changed, a deploy moved the route or someone disabled the endpoint. Detecting a gap means measuring an absence, and absence needs a baseline.
What the evidence looks like
The best evidence is a count of received events per source and event type per time bucket, with zeros included:
hour (UTC) payment.succeeded invoice.paid endpoint 2xx endpoint 4xx
2026-11-06 08 142 96 238 0
2026-11-06 09 151 101 252 0
2026-11-06 10 0 0 0 61
2026-11-06 11 0 0 0 0This is an example. Hour 10 shows deliveries that arrived and were rejected with 4xx, which is a problem on your side such as a rotated signing secret or a validation bug. Hour 11 shows nothing at all, so either the sender gave up, stopped, or is not reaching you.
Sender behavior matters. Stripe's documentation says it attempts to deliver events for up to three days with exponential back off in live mode, and that if a destination is disabled or deleted when a retry is due, future retries are prevented. GitHub states that it does not automatically redeliver failed deliveries, that a delivery fails if your server takes longer than 10 seconds to respond, and that you should redeliver missed webhooks once your server is back. So a gap from one sender may fill in later by itself, and from another it never will.
How to detect it step by step
1. Log every delivery at the door, before any processing. Record source, event type, delivery ID, response status and time. Log rejected deliveries too, since a flood of 4xx is part of the same incident.
2. Alert on silence for a source. If a source normally delivers every few minutes, use the time since the last accepted event:
time() - max by (source) (webhook_last_accepted_timestamp_seconds) > 900For a metric that disappears entirely, Prometheus's absent_over_time() returns 1 when a series has no samples in the window.
3. Compare each hour with the same hour last week. Quiet periods make fixed windows unreliable, so use ratios against history:
SELECT b.bucket, b.event_type, coalesce(c.n, 0) AS received, coalesce(p.n, 0) AS last_week
FROM buckets b
LEFT JOIN counts c ON c.bucket = b.bucket AND c.event_type = b.event_type
LEFT JOIN counts p ON p.bucket = b.bucket - INTERVAL '7 days' AND p.event_type = b.event_type
WHERE b.bucket > now() - INTERVAL '6 hours'
AND coalesce(p.n, 0) >= 20 AND coalesce(c.n, 0) < 0.2 * p.nThe buckets table needs one row per hour and event type so empty hours appear as rows rather than as missing data.
4. Track the rejection rate. Alert when 4xx or 5xx responses from your endpoint exceed a few percent of deliveries for a source.
5. Reconcile against the source of truth. Periodically ask the sender's API for events or objects in a window and compare with what you received. A mismatch finds partial gaps that volume alerts cannot, such as one event type missing while others flow. Replay what you missed.
6. Run a canary where the sender allows it. If you can trigger a test event on a schedule, a missing canary proves the path is broken within minutes, even for a source with no organic traffic.
Thresholds and pitfalls
Scale the silence limit to the source: three times the typical gap between events, with a floor. A source that sends ten events a day needs hours, not minutes. Pair a silence rule with a ratio rule so you catch both total loss and a partial drop.
Pitfalls:
- Nighttime and weekends. Low traffic makes zero a normal count. Use the same-hour-last-week baseline and a minimum expected count.
- Event-type confusion. An integration setting that changes which events are sent looks like a gap. Check subscriptions when the drop is limited to one type.
- Your own response time. Slow handlers cause timeouts on the sender's side, which then retries. A spike in duplicates is the sibling symptom. Acknowledge fast and process asynchronously.
- Clock and bucket edges. Events near an hour boundary move between buckets. Use windows wide enough that the edge does not matter.
- Ingestion lag. If your own logs are delayed, the gap may be in the pipeline. Check log freshness first.
What to do when it fires
Split the cause quickly. Rejected deliveries mean fix your endpoint first, then redeliver from the sender's dashboard or API. No deliveries and a healthy endpoint means check the sender's status page, the subscription and the endpoint URL. Then reconcile: pull the missing window from the sender's API and process it with idempotent handlers keyed on the delivery or event ID.
Related guides: missed schedules uses the same absence logic for cron jobs, vendor degradation covers slow calls to a provider and reconciliation mismatches covers the follow-up check. See Webhook integrations, plus Stripe and GitHub for the senders discussed here.
With Sazabi
Sazabi detects this today with no setup: the Sazabi agent automatically sets up sensors for your project and maintains them over time. Webhook Gap Alerts: An Issue When Events You Expect Stop Arriving covers what it catches.
Frequently Asked Questions
How do I know if I am missing webhooks?
Count received events per source, event type and hour, including zero counts, and compare with the same hour last week. Reconcile against the sender's API to find partial gaps.
Will the sender retry missed webhooks?
It depends on the sender. Stripe retries for up to three days in live mode, according to its documentation. GitHub does not automatically redeliver failed deliveries, so you must redeliver them.
Why do webhooks arrive but get rejected?
Common causes are a rotated signing secret, a changed route, validation errors and handlers that exceed the sender's timeout. Log every delivery and its response status.
How should I handle duplicates after replaying events?
Make handlers idempotent by keying on the delivery or event ID, so replays and sender retries do not process the same event twice.
Sources
- Stripe retries event delivery for up to three days with exponential back off in live mode; retries are prevented for a disabled or deleted destination; return a 2xx quickly (accessed Nov 8, 2026)
- GitHub does not automatically redeliver failed deliveries; a delivery fails if the server takes longer than 10 seconds (accessed Nov 8, 2026)
- Redeliver missed webhooks once the server is back up; respond within 10 seconds (accessed Nov 8, 2026)
- absent_over_time returns 1 when the range vector has no samples (accessed Nov 8, 2026)