DEV Community

InstaWebhook
InstaWebhook

Posted on

Why Your Webhooks Keep Failing: A 2026 Debugging Guide

Webhooks Failing 5 Common Reasons How To Debug Webhook Timeouts
Why Your Webhooks Keep Failing: A 2026 Debugging Guide
Webhooks are the nervous system of the modern internet. Whether you're processing payments through Stripe, automating CI/CD with GitHub, or wiring up an AI agent workflow, your application depends on these push-based, server-to-server notifications to work in real time.

The problem is that webhooks fail silently. There's rarely a loud error message — just missing data, stuck automations, and a support ticket from a confused customer. Because delivery is initiated by someone else's server, you're often blind to half of the transaction: you can see what happened on your end, but not what the sender actually experienced.

Here are the five most common reasons webhooks fail, how to debug each one, and what's actually documented (not assumed) by the major providers as of mid-2026.

  1. DNS Resolution Failures (The "Ghost" Endpoint) Before a provider can deliver an event, it has to resolve your domain to an IP address. If that lookup fails, the request never leaves the provider's network — not a single byte reaches you.

Why it happens

Localhost or private IPs. A webhook URL that still points to localhost, 127.0.0.1, or an internal 10.x.x.x address will be rejected outright, since providers operate over the public internet.
Typos. A .co instead of .com, or a missing subdomain, produces an unresolvable address.
Stale DNS. After migrating servers or updating A records, some providers' resolvers may still be querying cached records until propagation finishes.
How to debug it

Since the request never touches your infrastructure, your application logs will be empty — that's actually a diagnostic clue, not a dead end.

Check the provider's own delivery logs. Slack's Events API, for example, explicitly reports a connection_failed error when it "couldn't seem to connect to your server," which covers both DNS failures and unreachable hosts.
Confirm public resolution yourself:
Code example
Copy code
dig +short api.yourdomain.com
Point the endpoint at a public request-catcher (like Webhook.site) temporarily to confirm the provider can reach the open internet at all.

  1. Unhandled Exceptions Returning 500s (The "Crash and Burn") The payload makes it all the way to your server, but your handler throws, and you return a 500. What happens next depends heavily on which provider you're talking to — and this is where a lot of blog posts get sloppy.

It's not universally true that every provider retries. Retry behavior is provider-specific:

Provider Auto-retries on failure? Retry window
Stripe (live mode) Yes Up to 3 days, exponential backoff
Stripe (sandbox/test mode) Yes 3 attempts over a few hours
Shopify Yes 8 attempts over 4 hours, exponential backoff (updated Sept 2024)
Slack (Events API) Yes 3 attempts over ~1 hour
GitHub No GitHub does not automatically redeliver failed webhook deliveries — you must manually redeliver or script it yourself
That GitHub behavior surprises a lot of developers who assume "at-least-once delivery" is a universal webhook guarantee. It isn't. If your GitHub App or webhook integration throws a 500, that event is simply gone unless you catch it via the delivery history and redeliver it yourself.

Why it happens

Schema drift. A provider adds, removes, or retypes a field, and strict deserialization throws.
Null access. Code reaches into payload.customer.address.zip without checking that address exists.
Database constraint violations. A duplicate insert, a missing foreign key.
How to debug it

Log the raw body first, before parsing. If parsing itself is what's failing, you need to have captured the raw bytes before that point or you'll have nothing to inspect.
Wrap the handler in try/catch. Return a 2xx regardless, log the stack trace internally, and route the failed payload to a dead-letter queue for manual review — don't let a bad payload turn into a retry storm.
Deduplicate on event ID. Stripe's own docs note that in some cases two separate Event objects can be generated for what looks like one logical change — so for robust deduplication, key on both the event ID and the type + underlying object ID, not event ID alone.

  1. Timeouts (The Most Common Cause of "Random" Failures) If you're chasing intermittent, hard-to-reproduce failures, this is almost always the culprit. Every major provider enforces an aggressive response window, and the specific number varies more than most articles admit:

Provider Documented timeout Source
GitHub 10 seconds (hard limit, connection terminated after) GitHub Docs
Slack (Events API) 3 seconds Slack Developer Docs
Shopify 1-second connection timeout + 5-second total request timeout Shopify Dev Docs
Stripe No fixed number published; official guidance is simply "quickly return a 2xx prior to any complex logic" Stripe Docs
Slack's 3-second window is the tightest in mainstream use, and it's a common source of failures for anything beyond a simple acknowledgment. Shopify's combined 1-second connection + 5-second total limit trips up a lot of serverless deployments, where cold starts alone can eat 2–3 seconds before your code even runs.

Why it happens

Timeouts are a direct result of doing real work synchronously inside the request path — database writes, PDF generation, calls to an LLM or CRM, outbound email. Any one of these can push you past the limit, especially under load.

How to debug it

The rule holds regardless of provider: acknowledge fast, process asynchronously. Verify the signature, enqueue the raw payload, and return 200 — then let a background worker do the actual work.

Anti-pattern (synchronous, will eventually time out):

Code example
Copy code
app.post('/webhook/stripe', async (req, res) => {
const event = req.body;

// Heavy work directly in the request path — dangerous
await generateInvoice(event);
await sendCustomerEmail(event);
await updateDatabase(event);

res.status(200).send('Success'); // may never be reached in time
});
Better pattern (acknowledge first, process later):

Code example
Copy code
app.post('/webhook/stripe', async (req, res) => {
const event = req.body;

verifyWebhookSignature(req); // fast, must happen synchronously
await messageQueue.add('process-stripe-event', event); // fast
res.status(200).send('received'); // well under any provider's window
});
One more failure mode worth knowing: Stripe explicitly treats HTTP redirects (3xx) as delivery failures, not successes — a trailing-slash or www redirect on your webhook route is enough to break delivery even if your server is otherwise healthy and fast.

  1. Firewall and Security-Layer Blocks (The "Bouncer") Sometimes your application code is fine, but the request never reaches your router — it's stopped at the edge.

Why it happens

Webhooks are automated server-to-server POST requests: no browser User-Agent, no JavaScript execution to pass bot challenges, posting directly to an endpoint. To a strictly configured WAF (Cloudflare, Sucuri, a hosting provider's firewall) or a plugin like Wordfence, that pattern looks like a bot. The result is usually a 403 or 401 sent back to the provider. Global CSRF middleware causes the same symptom, since the provider obviously can't supply a CSRF token.

How to debug it

If the provider's dashboard shows 403/401 but your application logs are empty, a firewall or middleware layer is intercepting the request before your handler runs.
Allowlist the provider's published IP ranges. This is an officially recommended control, not just a workaround — Stripe's own documentation recommends combining IP allowlisting with signature verification as the two layers of protection, and publishes its current IP list for that purpose. GitHub, Slack, and Shopify all publish equivalent IP lists or metadata endpoints.
Exempt the webhook route from CSRF protection. Stripe's docs give this as a named best practice for Rails/Django-style frameworks, since HMAC signature verification already provides tamper protection — a CSRF token adds nothing.

  1. SSL/TLS Certificate Errors (The "Trust Issue") Nearly every provider requires HTTPS, but a certificate that merely exists isn't the same as one that's trusted.

Why it happens

Self-signed certificates. Fine for local development, rejected outright in production.
Missing intermediate chain. Your certificate might be valid, but if the intermediate certificates linking it to a trusted root aren't served alongside it, strict clients fail the handshake.
Expired certificates. A silently failed Let's Encrypt renewal takes down webhook delivery immediately, often before anyone notices the site itself still "looks" fine in a browser (which aggressively caches chains).
Outdated TLS versions. Stripe, for example, explicitly requires TLS 1.2 or 1.3 — anything older is rejected at the handshake.
How to debug it

Providers flag this distinctly from other errors — Stripe's delivery table shows a dedicated "TLS error" status, and Slack's Events API has a specific ssl_error code, separate from connection_failed or http_timeout.
Run an external audit rather than trusting your browser, which caches intermediate certs and can hide misconfigurations. SSL Labs is the tool Stripe itself points developers to.
Serve the full chain (leaf + intermediate certificates), not just your domain certificate.
A Note on Signing: The "Standard Webhooks" Movement
One thing that has genuinely shifted in the last year or two: webhook signing is converging on a common pattern instead of every provider inventing its own. The open Standard Webhooks specification — backed by companies including Svix — defines a consistent format for HMAC-SHA256 signing, using a timestamp plus an ID plus the raw payload as the signed message, with headers like webhook-id, webhook-timestamp, and webhook-signature.

It's not universal yet — Stripe, GitHub, and Shopify still use their own header names and payload-construction rules — but if you're building a product that sends webhooks rather than just consuming them, adopting the spec means integrators can reuse existing verification libraries instead of writing bespoke HMAC code for your API specifically.

Whichever scheme you use, the fundamentals stay the same:

Compute the HMAC over the raw, unparsed body — re-serializing JSON before hashing is one of the most common self-inflicted verification failures, since key ordering and whitespace aren't guaranteed to round-trip identically.
Compare signatures with a constant-time comparison function, never == or ===, to avoid leaking timing information.
Reject stale timestamps to prevent replay attacks (Stripe's default tolerance is 5 minutes).
A Debugging Checklist
DNS resolves publicly to a non-private IP
Endpoint returns a 2xx in under the provider's window (Slack: 3s, Shopify: 5s, GitHub: 10s, Stripe: as fast as possible)
Heavy work (DB writes, emails, third-party API calls) happens after the response, via a queue
Raw request body is logged before parsing
All exceptions are caught; failures route to a dead-letter queue instead of crashing
Events are deduplicated by ID (and, where relevant, by object ID + type)
Provider IP ranges are allowlisted at the firewall
The webhook route is exempt from CSRF middleware
TLS certificate chain is complete, unexpired, and TLS 1.2+
Signature verification runs against the raw body with a constant-time comparison
Where a Tool Like InstaWebhook Fits
Manually debugging webhook failures is hard precisely because you're blind to one side of the transaction — your server logs won't show a DNS failure, a firewall block, or a TLS handshake that never completed.

InstaWebhook sits in front of your endpoint as a durable intake layer: it accepts and stores the event first, then queues delivery to your actual infrastructure outside the request path. Each event's delivery timeline shows the received, queued, attempted, retried, delivered, or dead-lettered state with timestamps, so when a delivery fails, you can see exactly what your destination returned (and when) rather than reconstructing it after the fact. Failed events land in a dead-letter queue for inspection and replay instead of disappearing, and outgoing deliveries are signed with timestamped HMAC headers your receivers can verify. For teams that need payload data to stay in their own infrastructure, it also supports a "bring your own database" mode.

It won't fix a misconfigured firewall or an expired certificate for you — but it will tell you, quickly, which of the five failure modes above you're actually dealing with, instead of leaving you to guess.

Top comments (0)