Skip to main content

Retry Logic

NotifyKit automatically retries failed webhook deliveries. This page explains how retries work and how to handle failures.

Automatic Retries

When a webhook delivery fails, NotifyKit re-queues it with exponential backoff:

AttemptDelay before retry
1stImmediate
2nd~2 seconds
3rd~4 seconds

After 3 failed attempts, the job moves to failed status and retries stop.

When NotifyKit Retries

ConditionRetried?
5xx response from your endpointYes
Network error or connection refusedYes
Request timeoutYes
4xx response (400, 401, 403, 404, etc.)No
2xx responseNo (success)
4xx errors are not retried

A 4xx response means your endpoint rejected the request. Retrying the same payload to the same endpoint would produce the same result. Fix the endpoint configuration or payload instead.

Checking Job Status After Delivery

After sending a webhook, poll the job endpoint to confirm delivery:

const job = await client.sendWebhook({
url: "https://your-app.com/webhook",
payload: { event: "order.placed", orderId: "123" },
idempotencyKey: "order-123-webhook",
});

// Check status after a short delay
setTimeout(async () => {
const status = await client.getJob(job.jobId);

switch (status.status) {
case "completed":
console.log(`Delivered after ${status.attempts} attempt(s)`);
break;
case "failed":
console.error(
`Failed after ${status.attempts} attempt(s): ${status.errorMessage}`,
);
break;
case "processing":
console.log("Still in progress...");
break;
case "pending":
console.log("Waiting in queue...");
break;
}
}, 5000);

Listing Failed Jobs

To audit all failed deliveries:

const { data, pagination } = await client.listJobs({
status: "failed",
type: "webhook",
limit: 50,
});

data.forEach((job) => {
console.log(`Job ${job.id} failed: ${job.errorMessage}`);
console.log(`Attempted ${job.attempts} time(s), last at ${job.completedAt}`);
});

Or via the REST API:

curl "https://api.notifykit.dev/api/v1/notifications/jobs?status=failed&type=webhook" \
-H "X-API-Key: nh_your_key_here"

Manual Retry

You can manually re-queue any job with failed status:

try {
const result = await client.retryJob("job_xyz789");
console.log(result.message); // "Job has been re-queued for processing"
console.log(result.status); // "pending"
} catch (error) {
if (error instanceof NotifyKitError && error.isStatus(404)) {
console.error("Job not found or not in failed status");
}
}

REST API:

curl -X POST https://api.notifykit.dev/api/v1/notifications/jobs/job_xyz789/retry \
-H "X-API-Key: nh_your_key_here"

Only jobs with failed status can be retried. Jobs in pending, processing, or completed status cannot.

Preventing Duplicate Delivery

Use idempotencyKey to ensure a notification is only sent once, even if your code retries the send request due to a network error:

await client.sendWebhook({
url: "https://your-app.com/webhook",
payload: { orderId: "order_123" },
idempotencyKey: "order-123-fulfillment-webhook", // Unique per logical event
});

If you call sendWebhook again with the same idempotencyKey, the API returns 409 Conflict and does not create a new job. The first job (and its delivery) are not affected.

Good idempotency key patterns:

order-{orderId}-confirmation
user-{userId}-welcome-email
payment-{paymentId}-receipt
invoice-{invoiceId}-reminder

Designing Your Webhook Endpoint for Reliability

For webhook endpoints that receive NotifyKit deliveries:

  1. Return 2xx quickly — Process the payload asynchronously. A slow endpoint may time out and trigger a retry.
  2. Handle duplicates — Even with idempotency keys, design your endpoint to be idempotent. Store processed event IDs and skip duplicates.
  3. Return 200 for known bad payloads — If the payload is malformed but expected, return 200 to prevent retries. Log the issue separately.
  4. Use HTTPS — Ensures the payload is encrypted in transit.

Next Steps