GitHub Troubleshooting

GitHub Webhook Not Delivering: Common Causes and Fixes

Work through the most common reasons a GitHub webhook appears not to be delivering, from wrong URLs and secret mismatches to endpoint failures and localhost routing gaps.

The most common reason a GitHub webhook stops delivering is that the target URL is wrong, unreachable, or returns a non-2xx HTTP status code. Before you change any application code, open the Recent Deliveries tab inside your repository’s webhook settings and check what GitHub actually sent and what response it received. Nine times out of ten the answer is already there. This guide walks you through a structured debugging process so you can isolate the failure layer, fix the root cause, and get deliveries flowing again without guesswork.

The three-layer debugging model

Webhook delivery is a chain with three distinct segments. Identifying which segment broke is the fastest path to a fix. If you skip ahead to debugging your handler code before confirming that the request even arrived, you can waste hours chasing a problem that does not exist in your code at all.

Layer 1 — Did GitHub actually send it?

GitHub logs every delivery attempt. Navigate to Settings > Webhooks > Recent Deliveries in your repository (or organization). Each entry shows the HTTP status code returned by your endpoint, the full request payload, and the response body. If the list is empty for the event you expected, the webhook is either disabled, misconfigured for different events, or GitHub experienced a rare platform delay.

Layer 2 — Did the public endpoint receive it?

If GitHub shows a delivery but your application has no record of it, the problem sits between GitHub and your server. A webhook debugger like HookNexus gives you an independent capture point: GitHub sends to a HookNexus URL, and you can verify receipt on the HookNexus dashboard regardless of whether your own server is healthy. This is the single most effective way to separate network and infrastructure problems from application-level bugs.

Layer 3 — Did your local handler process it?

Once you confirm the request reached the public endpoint, the remaining possibilities are local: your forwarding tunnel is down, your application threw an exception, or a middleware layer (authentication, signature verification) rejected the request before your handler ran. Check your application logs and the forwarding guide for details.

Most common causes

Below are the six issues that account for the vast majority of “webhook not delivering” reports. Work through them in order; each one builds on ruling out the previous.

1. The webhook URL is wrong or stale

This is the number-one cause. It happens when you copy a URL from a development environment, rotate a HookNexus endpoint, or change domains without updating GitHub. Open your repository’s webhook configuration and compare the Payload URL character by character against the URL your endpoint is actually listening on. Pay special attention to trailing slashes, protocol (https vs. http), port numbers, and path segments.

If you are using HookNexus, the correct URL format is:

https://api.hooknexus.com/h/<your-endpoint-id>

Any deviation — a missing /h/ prefix, an old endpoint ID, a typo in the domain — means GitHub sends the request into a void.

2. The endpoint returns a non-2xx status code

GitHub considers any response outside the 200-299 range a failure. Common offenders include:

  • 301/302 redirects — GitHub does not follow redirects on webhook deliveries.
  • 401/403 — An authentication middleware rejects the request before your handler sees it.
  • 404 — The route does not exist on the target server.
  • 500 — Your handler throws an unhandled exception.

A healthy webhook handler should return 200 OK as quickly as possible, ideally before doing any heavy processing. Offload slow work to a background queue. Here is a minimal Express handler that does this correctly:

app.post("/webhooks/github", (req, res) => {
  // Respond immediately so GitHub sees a 200
  res.status(200).json({ received: true });

  // Process asynchronously
  setImmediate(() => {
    handleGitHubEvent(req.body).catch((err) => {
      console.error("Webhook processing failed:", err);
    });
  });
});

3. GitHub is sending but your endpoint does not capture it

Sometimes the Recent Deliveries tab in GitHub shows a 200 response, yet your application has no record of the event. This usually means something upstream acknowledged the request on your behalf — a load balancer health check route, a CDN edge function, or a catch-all middleware that returns 200 without forwarding the body. Check whether the 200 is genuinely coming from your handler or from infrastructure sitting in front of it.

4. Secret mismatch causes rejection

If you configured a webhook secret in GitHub, every delivery includes an X-Hub-Signature-256 header. Your application must compute the HMAC-SHA256 of the raw request body using the same secret and compare it to the header value. A mismatch — caused by rotating the secret on one side but not the other, or by a middleware that parses the body before your verification code reads the raw bytes — results in a rejected request. See the full walkthrough in Validate GitHub Webhook Signatures.

5. Localhost forwarding is broken

If you are developing locally and relying on a tunnel (HookNexus CLI, ngrok, or similar) to route traffic from a public URL to localhost, there are several points of failure:

  • The tunnel process is not running or has restarted with a new URL.
  • The local server is not running on the expected port.
  • A firewall or VPN is blocking the tunnel connection.
  • The tunnel URL has expired (common with free ngrok sessions).

Run a quick connectivity test from the machine where the tunnel terminates:

curl -X POST http://localhost:3000/webhooks/github \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

If this returns a connection error, your local server is not listening. If it returns a non-200 status, the problem is in your handler, not the tunnel.

6. The event type you need is not subscribed

GitHub webhooks can be configured to fire on specific events. If you only subscribed to push events but your workflow depends on pull_request or issues, those payloads will never arrive. Open the webhook configuration, scroll to Which events would you like to trigger this webhook?, and verify every event type your integration requires is checked.

Step-by-step troubleshooting process

Use this sequence when a webhook stops working. Each step either confirms a layer is healthy or reveals the exact failure point.

Step 1 — Check GitHub Recent Deliveries

Go to your repository on GitHub. Click Settings > Webhooks, select the webhook in question, and click the Recent Deliveries tab. Look at the most recent entries:

  • If there are no deliveries at all, confirm that the webhook is active (not paused) and that the triggering event actually occurred.
  • If deliveries exist, note the HTTP status code. A 200 means GitHub considers the delivery successful. Anything else is a clue.
  • Click into a specific delivery to see the full request headers, payload, and the response your endpoint returned.

Step 2 — Verify the endpoint URL is correct

Compare the Payload URL in GitHub with the actual URL your endpoint is serving. If you use HookNexus, open your dashboard and confirm the endpoint ID matches.

Step 3 — Test the endpoint independently

Send a manual request to the webhook URL from your terminal to rule out GitHub-specific issues:

curl -i -X POST https://api.hooknexus.com/h/YOUR_ENDPOINT_ID \
  -H "Content-Type: application/json" \
  -H "X-GitHub-Event: ping" \
  -d '{"zen": "Responsive is better than fast."}'

A healthy endpoint returns 200. If you get a DNS error, connection timeout, or non-200 status, the problem is with the URL or the server, not with GitHub.

Step 4 — Check status codes and response body

If GitHub shows a delivery with a 4xx or 5xx status, read the response body GitHub recorded. It often contains an error message from your application or your infrastructure (e.g., “Unauthorized”, “Route not found”, “Internal Server Error”). Match the error to the corresponding cause listed above.

Step 5 — Verify event subscriptions

Still in the webhook settings, confirm that every event type your integration needs is selected. A common mistake is leaving the default “Just the push event” selected when your code listens for pull_request, release, or workflow_run.

Step 6 — Check local forwarding

If you are forwarding from a public endpoint to localhost:

  1. Confirm the tunnel or CLI process is running and shows the correct public URL.
  2. Confirm your local server is running and listening on the correct port.
  3. Send a test request directly to localhost (see the curl command in cause number 5 above).
  4. Check your application logs for incoming requests and any exceptions.

For a deeper dive into the full GitHub webhook debugging workflow, including replay and payload inspection, see the dedicated guide.

Using HookNexus to isolate the problem

The capture-then-forward workflow eliminates guesswork by splitting delivery into two observable halves.

Step 1: Point GitHub at a HookNexus endpoint. Create an endpoint in the HookNexus dashboard, copy the URL, and paste it into your GitHub webhook configuration. Every delivery from GitHub now appears on the dashboard with full headers, body, and timing — regardless of what your local code does.

Step 2: Inspect the captured request. If the request shows up on the dashboard, GitHub’s side is working. If it does not, revisit the URL, event subscriptions, and GitHub’s own delivery logs.

Step 3: Forward to localhost. Use the HookNexus CLI to forward captured requests to your local development server:

hooknexus listen <endpoint-id>
hooknexus forward <endpoint-id> --to http://localhost:3000/webhooks/github

Now you can see exactly what your local handler receives. If forwarding fails, the problem is your tunnel or local server. If forwarding succeeds but your handler errors, the problem is in your application code. This two-step separation is the fastest path to a root-cause diagnosis. Refer to the HookNexus GitHub integration docs for full setup instructions.

Quick checklist

Run through these items whenever a GitHub webhook is not delivering. Each one takes less than a minute to verify.

  • The webhook is set to Active in GitHub settings
  • The Payload URL exactly matches your live endpoint (no typos, correct protocol, correct path)
  • The endpoint returns a 200 status when you send a manual curl request
  • The webhook secret in GitHub matches the secret your application uses for HMAC verification
  • All required event types are selected in the webhook configuration
  • Your local server is running and the forwarding tunnel is active (if developing locally)
  • No middleware or proxy is intercepting the request and returning a non-200 before your handler

Frequently asked questions

Why does GitHub show a 200 but my application does not process the event?

A 200 in Recent Deliveries means something at your endpoint’s URL acknowledged the request. That something might not be your handler — it could be a load balancer health check, a catch-all route, or a CDN edge function. Check your application logs to confirm the request actually reached your webhook handler code. If you use HookNexus, compare the captured payload on the dashboard with what your application logged.

How long does GitHub wait before marking a delivery as failed?

GitHub imposes a 10-second timeout on webhook deliveries. If your endpoint does not respond within that window, GitHub records the delivery as a timeout failure and will retry according to its retry policy. This is why your handler should return 200 immediately and process the payload asynchronously. Heavy synchronous work — database writes, external API calls, file operations — should be moved to a background job.

Can I manually re-trigger a failed webhook delivery?

Yes. In the Recent Deliveries tab, click on any delivery and select Redeliver. GitHub sends the exact same payload to the currently configured URL. This is useful for testing fixes, but note that it sends to the current URL, not the URL that was configured at the time of the original delivery. For more flexible replay capabilities, including modifying headers and routing to different endpoints, use the HookNexus replay feature.

Does GitHub retry failed deliveries automatically?

GitHub retries failed deliveries (non-2xx responses or timeouts) automatically. The retry schedule is not publicly documented, but deliveries are typically retried a small number of times over a period of hours. You should not rely on retries as a substitute for fixing the root cause, because retries are not guaranteed and stop after a limited number of attempts.

Next steps

Once your webhook is delivering reliably, harden your setup to prevent future failures:

  • Add signature verification to prevent unauthorized payloads from reaching your handler. The signature validation guide covers the implementation step by step.
  • Set up monitoring in the HookNexus dashboard so you get alerted when deliveries fail, rather than discovering the problem hours later.
  • Browse the GitHub webhook guides in the GitHub learning section for topics like debugging payload content, handling specific event types, and testing webhooks during CI.
  • Read the integration documentation at the HookNexus docs site for advanced configuration including team endpoints and custom routing rules.