Webhook Guides
GitHub Webhook Retry Strategy
A GitHub webhook retry strategy should separate provider redelivery from FastHook destination recovery. GitHub can redeliver a provider delivery, but GitHub does not automatically redeliver failed deliveries. FastHook retry rules, event retry, request retry, and bulk retry operate after FastHook has accepted or stored the request.
The recovery rule is simple: prove where the failure happened, fix that layer, then retry the smallest safe slice. For GitHub automation, that means idempotency before replay, narrow filters before bulk operations, and attempt evidence before touching production deploys or pull request side effects.
Retry and redelivery model
GitHub redelivery sends the provider request again from GitHub. FastHook automatic retry schedules another destination attempt for an accepted event. FastHook event retry queues one existing event branch again. FastHook request retry replays the stored inbound request through current routing again.
| Path | What happens | Use when | Trigger | Evidence |
|---|---|---|---|---|
| GitHub redelivery | GitHub sends the provider delivery again | FastHook never received the request, or you want to test provider-side delivery | GitHub UI or REST API | New provider delivery attempt with GitHub headers |
| FastHook automatic retry | FastHook schedules another destination attempt for the same event | Receiver returns retryable status or has a transport failure | Connection retry rule | Attempt trigger AUTOMATIC |
| FastHook event retry | FastHook queues one existing event branch again | Routing was correct, but one destination failed | POST /v1/events/{id}/retry | New replay event id, attempt trigger MANUAL |
| FastHook request retry | FastHook replays the stored inbound request through current routing | Filters, routing, transformations, or source-level handling changed | POST /v1/requests/{id}/retry | New retry request through source pipeline |
| Bulk retry | FastHook creates a batch operation from a narrow filter | Many requests or events need the same recovery action | POST /v1/events/bulk_operations or /v1/requests/bulk_operations | Attempt trigger BULK_RETRY for event bulk retry |
FastHook automatic retry rule
A retry rule belongs to a connection. It only controls destination delivery after a request was accepted and routed into an event. count is the number of retry attempts after the first destination attempt.interval is milliseconds; exponential retry uses interval * 2^(attempt - 1).
{
"type": "retry",
"strategy": "exponential",
"interval": 30000,
"count": 3,
"response_status_codes": ["429", "500-599"]
}Create a GitHub connection with retry
This connection retries throttling and server failures for GitHub automation. Permanent receiver contract errors should usually fail fast until the receiver or transformation is fixed.
curl -X POST "https://api.fasthook.io/v1/connections" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "github-main-automation",
"source_id": "src_q6z62b6py5o79b",
"destination_id": "des_TU9ioCk5EHUU",
"rules": [
{
"type": "filter",
"headers": {
"x-github-event": { "$in": ["push", "pull_request"] }
},
"body": {
"$or": [
{ "ref": "refs/heads/main" },
{ "pull_request": { "base": { "ref": "main" } } }
]
}
},
{
"type": "retry",
"strategy": "exponential",
"interval": 30000,
"count": 3,
"response_status_codes": ["429", "500-599"]
}
]
}'Status code matching
FastHook accepts exact HTTP codes, ranges, comparisons, and multiple expressions in response_status_codes. Transport failures can be retried by the rule because there is no HTTP status to match.
| Pattern | Example | Use |
|---|---|---|
| Exact status | 429 | Retry only one known recoverable status. |
| Range | 500-599 | Retry temporary server failures. |
| Comparison | >=500 | Retry all 5xx responses. |
| Multiple values | 429, 503, 504 | Retry throttling and gateway failures. |
| Omitted list | null | Retry any failed HTTP status when a retry rule exists. |
Inspect before retry
Before retrying, inspect events and attempts. Events tell you whether work is queued, scheduled, held, failed, or successful. Attempts show the receiver response body, status, trigger, method, URL, and latency.
curl -G "https://api.fasthook.io/v1/events" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
--data-urlencode "source_id=src_q6z62b6py5o79b" \
--data-urlencode "destination_id=des_TU9ioCk5EHUU" \
--data-urlencode "status=FAILED" \
--data-urlencode "from=now-24h" \
--data-urlencode "to=now" \
--data-urlencode "limit=50" \
--data-urlencode "q=github"curl -G "https://api.fasthook.io/v1/events" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
--data-urlencode "destination_id=des_TU9ioCk5EHUU" \
--data-urlencode "status=SCHEDULED" \
--data-urlencode "from=now-2h" \
--data-urlencode "to=now" \
--data-urlencode "limit=50"curl -G "https://api.fasthook.io/v1/attempts" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
--data-urlencode "event_id=evt_01JYGITHUB" \
--data-urlencode "order_by=created_at" \
--data-urlencode "dir=asc" \
--data-urlencode "limit=20"{
"count": 2,
"models": [
{
"id": "att_01JYFIRST",
"event_id": "evt_01JYGITHUB",
"destination_id": "des_TU9ioCk5EHUU",
"attempt_number": 1,
"trigger": "INITIAL",
"status": "FAILED",
"response_status": 503,
"body": {
"error": "deploy queue unavailable"
},
"http_method": "POST",
"requested_url": "https://api.example.com/webhooks/github"
},
{
"id": "att_01JYSECOND",
"event_id": "evt_01JYGITHUB",
"destination_id": "des_TU9ioCk5EHUU",
"attempt_number": 2,
"trigger": "AUTOMATIC",
"status": "SUCCESSFUL",
"response_status": 200,
"body": {
"ok": true
},
"http_method": "POST",
"requested_url": "https://api.example.com/webhooks/github"
}
]
}Choose the smallest safe recovery path
Retry one event first when the original routing decision was correct. Retry one request when current routing should be evaluated again. Use bulk retry only after a single retry proves the receiver is healthy.
Retry one failed event branch
curl -X POST "https://api.fasthook.io/v1/events/evt_01JYGITHUB/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"{
"ok": true,
"queued": true,
"transport": "queue",
"retried_at": "2026-05-31T10:45:12.120Z",
"event_id": "evt_01JYGITHUB",
"replay_event_id": "evt_01JYREPLAY"
}Replay one request through current routing
curl -X POST "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"{
"ok": true,
"queued": true,
"transport": "queue",
"replayed_at": "2026-05-31T10:46:04.232Z",
"request_id": "req_8bd1e3f2a4c94201",
"source_id": "src_q6z62b6py5o79b",
"replay_message_id": "rpl_7ca31a0f7aa14b14a6a1",
"original_rejection_cause": null
}Create a narrow event bulk retry
curl -X POST "https://api.fasthook.io/v1/events/bulk_operations" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-05-31T10:00:00.000Z",
"to": "2026-05-31T10:30:00.000Z",
"status": "FAILED",
"source_id": "src_q6z62b6py5o79b",
"destination_id": "des_TU9ioCk5EHUU",
"q": "github"
}'Create a narrow request bulk retry
curl -X POST "https://api.fasthook.io/v1/requests/bulk_operations" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-05-31T10:00:00.000Z",
"to": "2026-05-31T10:30:00.000Z",
"status": "accepted",
"source_id": "src_q6z62b6py5o79b",
"q": "refs/heads/main"
}'Cancel a bulk operation if failures rise
curl -X POST "https://api.fasthook.io/v1/events/bulk_operations/bch_01JYRETRY/cancel" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"GitHub redelivery
Use GitHub redelivery when FastHook never received the provider request or when you specifically need GitHub to send the delivery again. For repository webhooks, GitHub exposes a REST endpoint that creates another delivery attempt for a delivery id.
curl -L -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer github_pat_xxx" \
-H "X-GitHub-Api-Version: 2026-03-10" \
"https://api.github.com/repos/OWNER/REPO/hooks/HOOK_ID/deliveries/DELIVERY_ID/attempts"Event status and attempt triggers
Event status explains where work is waiting or why it stopped. Attempt trigger explains why the destination was called: initial delivery, automatic retry, manual event retry, bulk retry, or unpause drain.
| Status | Meaning |
|---|---|
QUEUED | Delivery work is waiting to be sent. |
SCHEDULED | Delivery is waiting on retry backoff, rate limit, or another scheduled send path. |
HOLD | The connection is paused and routed work is being held. |
FAILED | Retry rules were exhausted, no rule matched, or the destination failed without a retry path. |
SUCCESSFUL | A destination attempt completed successfully. |
CANCELLED | The event was cancelled or ignored by an operational control. |
GitHub idempotency keys
Any retry path can deliver duplicate business work. Store X-GitHub-Delivery for tracing, but guard side effects with event-specific keys that represent the business action.
export function githubIdempotencyKey(headers, payload) {
const eventType = headers["x-github-event"];
const deliveryId = headers["x-github-delivery"];
const repoId = payload.repository?.id ?? payload.repository?.full_name;
if (eventType === "push") {
return ["github", "push", repoId, payload.ref, payload.after].join(":");
}
if (eventType === "pull_request") {
return [
"github",
"pull_request",
payload.pull_request.id,
payload.action,
payload.pull_request.head?.sha
].join(":");
}
if (eventType === "workflow_run") {
return ["github", "workflow_run", payload.workflow_run.id, payload.workflow_run.status].join(":");
}
return ["github", eventType, deliveryId].join(":");
}Production recovery checklist
- Verify
X-Hub-Signature-256at source before accepting retryable GitHub traffic. - Return success only after your receiver has durably recorded the work or a dedupe no-op.
- Use retry rules for temporary 429, 503, 504, 5xx, and transport failures.
- Avoid retrying permanent 400, 401, 403, 404, and schema errors until the contract is fixed.
- Use event retry for one failed destination branch after the receiver is fixed.
- Use request retry when routing, filters, transformations, or source handling changed.
- Use GitHub redelivery only when the provider request itself must be sent again.
- Start bulk retry with a narrow time window, source, destination, status, and search term.
- Watch scheduled, queued, failed, successful, and held event counts while recovery drains.
GitHub webhook retry FAQ
Does GitHub automatically retry failed webhook deliveries?
No. GitHub records failed deliveries and lets you redeliver them manually or through the REST API, but GitHub does not automatically redeliver failed webhook deliveries.
How long can GitHub webhook deliveries be redelivered?
GitHub documents redelivery for webhook deliveries that occurred in the past 3 days on GitHub.com.
What does FastHook retry count mean?
FastHook retry count is the number of retry attempts after the first delivery attempt. For example, count 3 allows retry attempts 1, 2, and 3 after the initial attempt.
Should I use FastHook event retry or request retry?
Use event retry when one already-routed destination branch failed. Use request retry when the original request should run through current routing, filters, transformations, deduplication, and pause state again.
Is X-GitHub-Delivery enough for idempotency?
Use X-GitHub-Delivery for tracing, but add a business key such as repository id, commit SHA, pull request id, action, or workflow run id before triggering side effects.
GitHub documentation references
GitHub states that it does not automatically redeliver failed deliveries in handling failed webhook deliveries. GitHub documents manual and REST redelivery, including the 3-day redelivery window, in redelivering webhooks. The repository webhook REST reference includes the redelivery endpoint at REST API endpoints for repository webhooks.