Tutorials 8 min read

Top 5 Ways to Debug Webhook Delivery Issues

A practical guide to diagnosing and fixing webhook delivery failures — from request inspection and retry analysis to monitoring tools and payload validation.

H

HookWatch Team

March 24, 2026

Your webhook integration was working fine for weeks. Then one morning, events stop arriving. Or they arrive intermittently. Or they arrive but your handler returns a 500 and you're not sure why.

Debugging webhooks is uniquely frustrating because you're dealing with asynchronous, cross-system communication. The bug could be on the sender's side, the network, your infrastructure, or your application code — and you often don't control half of those layers.

Here are five practical approaches to systematically track down webhook delivery issues, starting with the quickest wins.

1. Inspect the Raw Request Before Your Code Touches It

The first rule of debugging webhooks: see exactly what arrives at your endpoint before your application processes it.

Most webhook bugs stem from a mismatch between what you expect and what actually shows up. The provider changed a field name, added a new event type, or wrapped the payload in an extra object. Your code crashes trying to parse it, and all you see is a generic 500 error.

Use a Request Inspection Tool

Before touching your code, point the webhook at a request bin to see the raw payload:

Bash
# Using a local inspection tool
npx localtunnel --port 3000
# or
ngrok http 3000

Then temporarily redirect the webhook URL to your tunnel. You'll see every header and the complete request body without any of your application logic interfering.

Log the Raw Request in Your Handler

If you can't redirect the webhook, add raw request logging at the very top of your handler — before any parsing or validation:

Go
func handleWebhook(c *fiber.Ctx) error {
    // Log BEFORE any processing
    log.Info("webhook_raw",
        "method", c.Method(),
        "path", c.Path(),
        "content_type", c.Get("Content-Type"),
        "body_length", len(c.Body()),
        "headers", c.GetReqHeaders(),
        "body", string(c.Body()[:min(len(c.Body()), 2000)]),
    )

    // Now proceed with normal processing
    // ...
}

Key things to check in the raw request:

  • Content-Type — is it application/json like you expect, or application/x-www-form-urlencoded?
  • Body encoding — is the JSON well-formed? Is it double-encoded (a string containing JSON rather than actual JSON)?
  • Event type header — many providers send the event type in a header (e.g., X-GitHub-Event, Stripe-Event-Type) that you might be filtering incorrectly
  • Signature headers — if you're verifying HMAC signatures, compare the signature header value with what you compute locally

A Real Example

A developer spent two hours debugging a Shopify webhook that "stopped working." Raw request logging revealed the issue in seconds: Shopify had started sending Content-Type: application/json; charset=utf-8 instead of application/json, and the handler's content type check was using strict equality instead of a prefix match.

Go
// Broken
if c.Get("Content-Type") != "application/json" {
    return c.Status(415).SendString("unsupported content type")
}

// Fixed
if !strings.HasPrefix(c.Get("Content-Type"), "application/json") {
    return c.Status(415).SendString("unsupported content type")
}

2. Check the Provider's Delivery Logs

Before assuming the problem is on your end, check whether the provider actually sent the webhook.

Most webhook providers offer delivery logs in their dashboard:

ProviderWhere to Find Logs
StripeDashboard → Developers → Webhooks → select endpoint → Recent deliveries
GitHubRepo → Settings → Webhooks → Recent Deliveries
ShopifySettings → Notifications → Webhooks → select webhook
TwilioConsole → Monitor → Logs → Error logs
SendGridActivity → search by event type

What to look for in provider logs:

  • HTTP status code your server returned — a 200/201/204 means your server acknowledged it. Anything else means either your server rejected it or it couldn't connect.
  • Response time — if the provider shows a timeout (usually 5-30 seconds depending on the provider), your handler is too slow.
  • Request body — compare what the provider sent with what your logs show. If they don't match, something in your infrastructure is modifying the request (a WAF, an API gateway, or a reverse proxy stripping headers).
  • Retry attempts — if the provider retried multiple times and all failed, the problem is persistent. If only the first attempt failed and a retry succeeded, it was likely a transient issue (deployment, scaling event, network blip).

3. Trace the Request Through Your Infrastructure

Webhooks often pass through multiple layers before reaching your application code. Each layer is a potential failure point.

A typical webhook request path:

Code
Provider → DNS → CDN/WAF → Load Balancer → Reverse Proxy → App Server → Handler

DNS Issues

Is your webhook domain resolving correctly? This sounds basic, but DNS propagation delays after a domain change or certificate renewal can cause intermittent failures.

Bash
# Check DNS resolution
dig +short api.yourapp.com

# Check from multiple locations
curl -s "https://dns.google/resolve?name=api.yourapp.com&type=A" | jq

TLS Certificate Issues

Expired or misconfigured TLS certificates cause webhook deliveries to fail silently from the provider's perspective. The provider's HTTP client refuses to connect, logs a TLS error, and moves on.

Bash
# Check certificate validity
echo | openssl s_client -servername api.yourapp.com -connect api.yourapp.com:443 2>/dev/null | openssl x509 -noout -dates

# Check the full chain
curl -vI https://api.yourapp.com/webhook 2>&1 | grep -E "SSL|certificate|expire"

WAF/Firewall Blocking

Web Application Firewalls sometimes block webhook requests because the payload looks suspicious — large JSON bodies, unusual user agents, or POST requests to paths that normally only serve GET requests.

Check your WAF logs for blocked requests to your webhook endpoints. Common false positives:

  • Webhook payloads containing HTML or script-like content (e.g., a GitHub webhook for a PR that modifies HTML files)
  • Large payloads exceeding the WAF's body size limit
  • Non-browser User-Agent strings (many providers send custom UAs like GitHub-Hookshot or Stripe/1.0)

Load Balancer Health Checks

If your load balancer marks your webhook handler's backend as unhealthy, it will return 502/503 to incoming requests. This can happen if your health check endpoint shares resources with your webhook handler and becomes slow under load.

4. Implement Retry-Aware Debugging

Understanding how retries work for each provider is critical for debugging.

Retry Schedules Vary Wildly

ProviderRetry StrategyMax AttemptsTotal Window
StripeExponential backoffUp to 12~3 days
GitHub10-second intervals3~30 seconds
ShopifyExponential backoff19~48 hours
TwilioExponential backoffVariableConfigurable

This matters for debugging because:

  • GitHub gives up after 30 seconds. If your endpoint was briefly down during a deployment, you permanently lost that event unless you have retry infrastructure on your side.
  • Stripe retries for 3 days. If you fix the bug within that window, the queued events will eventually arrive — which means you might process events out of order.

Idempotency During Debugging

When you're debugging and fixing a webhook handler, providers may have queued multiple retry attempts. Once your fix is deployed, all those retries land at once.

Make sure your handler is idempotent before deploying the fix:

Go
func handlePaymentWebhook(c *fiber.Ctx) error {
    var event PaymentEvent
    if err := c.BodyParser(&event); err != nil {
        return c.Status(400).SendString("invalid payload")
    }

    // Check if we've already processed this event
    exists, err := db.EventExists(event.ID)
    if err != nil {
        return c.Status(500).SendString("database error")
    }
    if exists {
        // Already processed — return 200 to stop retries
        return c.SendStatus(200)
    }

    // Process the event...
    return c.SendStatus(200)
}

Replay for Debugging

Some providers let you replay specific webhook deliveries from their dashboard. This is invaluable for debugging because you can:

  1. Fix the bug in your handler
  2. Deploy the fix
  3. Replay the failed delivery from the provider's UI
  4. Verify it succeeds

If your provider doesn't support replay, you need your own solution. This is where a webhook proxy becomes valuable — capturing the raw request so you can re-deliver it to your endpoint after fixing the bug.

5. Set Up Proactive Monitoring

Debugging after the fact is painful. The best approach is catching delivery issues before they become outages.

Health Check Endpoints

Create a dedicated health check that verifies your webhook processing pipeline end-to-end:

Go
func webhookHealthCheck(c *fiber.Ctx) error {
    checks := map[string]bool{
        "database":    checkDatabaseConnection(),
        "redis":       checkRedisConnection(),
        "disk_space":  checkDiskSpace(),
        "queue_depth": checkQueueDepth() < 10000,
    }

    allHealthy := true
    for _, healthy := range checks {
        if !healthy {
            allHealthy = false
            break
        }
    }

    if !allHealthy {
        return c.Status(503).JSON(checks)
    }
    return c.Status(200).JSON(checks)
}

Alerting on Error Rates

Rather than alerting on individual failures (too noisy), alert on error rates exceeding a threshold:

  • Warning at 5% failure rate over a 5-minute window
  • Critical at 20% failure rate or any 10-minute window with zero successful deliveries
  • Page at 100% failure rate for more than 2 minutes (endpoint is completely down)

End-to-End Monitoring with a Proxy

The most comprehensive approach is routing webhooks through a monitoring proxy that captures every request and response, independent of your application code. This gives you:

  • Full request/response logging even when your app crashes
  • Latency metrics from the proxy's perspective (not just your handler's)
  • Historical replay capability for any failed delivery
  • Unified visibility across all webhook providers

Tools like [HookWatch](https://hookwatch.dev) provide this proxy layer out of the box, along with alerting and a CLI for real-time event inspection. But even a self-built nginx or Envoy proxy with structured access logging covers a lot of ground.

A Debugging Flowchart

When a webhook stops working, follow this decision tree:

Code
1. Is the provider sending the webhook?
   → Check provider's delivery logs
   → No: Issue is on the provider side (configuration, event filters)
   → Yes: Continue

2. Is the request reaching your server?
   → Check access logs, WAF logs, load balancer logs
   → No: DNS, TLS, firewall, or infrastructure issue
   → Yes: Continue

3. Is your handler returning 2xx?
   → Check application logs
   → No: Your code has a bug — check raw request vs expected format
   → Yes: Continue

4. Is the event being processed correctly?
   → Check database, downstream effects
   → No: Logic bug in your handler (parsing, business logic, DB write)
   → Yes: The webhook is working — investigate elsewhere

Conclusion

Webhook debugging is difficult because the failure can occur at any point in a multi-system chain, and the asynchronous nature means you often discover problems long after they started.

The five strategies — raw request inspection, provider log checking, infrastructure tracing, retry-aware debugging, and proactive monitoring — form a complete toolkit. Start with the fastest diagnostic (check provider logs and raw requests) and escalate to deeper infrastructure investigation only when needed.

The best debugging session is the one you never have to start. Invest in monitoring and alerting early, and most webhook issues will be caught and resolved before they become the kind of mystery that eats an afternoon.

Tags: webhooksdebuggingdeveloper-toolsapitroubleshooting

Share this article

Ready to try HookWatch?

Start monitoring your webhooks in minutes. No credit card required.

Start Free Today