This document specifies the enhanced retry policy for failed webhook deliveries in the Fluxora backend. The policy ensures operator-grade reliability with predictable behavior for webhook consumers and clear failure modes for integrators.
The Fluxora backend guarantees:
- Durable delivery attempts - Failed webhooks are retried with configurable backoff strategies
- Outbox pattern - Reliable webhook delivery with transactional guarantees
- Dead-letter queue - Failed deliveries are preserved for inspection and manual retry
- Circuit breaker protection - Automatic protection against failing endpoints
- Predictable retry behavior - Consumers can rely on consistent retry timing and limits
- Deduplication support - Delivery IDs enable idempotent webhook processing
- Observable health - Operators can monitor delivery status and diagnose failures
- Secure delivery - HMAC-SHA256 signatures prevent tampering and replay attacks
{
maxAttempts: 5, // Maximum delivery attempts
initialBackoffMs: 1000, // First retry after 1 second
backoffMultiplier: 2, // Exponential backoff multiplier
maxBackoffMs: 60000, // Cap backoff at 60 seconds
jitterPercent: 10, // Set to 0 to disable jitter
timeoutMs: 30000, // 30 second timeout per attempt
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
backoffStrategy: 'exponential', // exponential | linear | fixed
jitterAlgorithm: 'full', // full | equal | decorrelated
previousDelayMs: 2000, // Optional state for decorrelated jitter
deadLetterAfterMs: 3600000, // Send to DLQ after 1 hour (optional)
circuitBreakerThreshold: 10, // Open circuit after 10 failures (optional)
circuitBreakerResetMs: 300000 // Reset circuit after 5 minutes (optional)
}- Delay = initialBackoffMs * (multiplier ^ attemptNumber)
- Example: 1s, 2s, 4s, 8s, 16s (capped at maxBackoffMs)
- Delay = initialBackoffMs + (attemptNumber * initialBackoffMs)
- Example: 1s, 2s, 3s, 4s, 5s
- Delay = initialBackoffMs (constant)
- Example: 1s, 1s, 1s, 1s, 1s
- Random delay between 0 and calculated backoff
- Prevents thundering herd effectively
- Honors maxBackoffMs because the raw backoff is capped before jitter
- Delay = (backoff/2) + random(0, backoff/2)
- Balances load spreading and promptness
- Delay = random(initialBackoffMs, min(maxBackoffMs, previousDelayMs * 3))
- Adapts to varying load conditions
- generateRetrySchedule() carries previousDelayMs forward between attempts; direct calculateNextRetryTime() callers can pass previousDelayMs when they need deterministic decorrelated chaining.
With the default policy, retry attempts occur at:
- Attempt 1: Immediate
- Attempt 2: ~1 second delay
- Attempt 3: ~2 seconds delay
- Attempt 4: ~4 seconds delay
- Attempt 5: ~8 seconds delay
- Attempt 6: ~16 seconds delay (if configured)
Total maximum delivery window: ~31 seconds
Trusted for:
- Valid request shape and format
- Possession of webhook secret (if configured)
Not trusted for:
- Payload integrity (verified via signature)
- Replay prevention (verified via timestamp)
- Skipping signature checks
Trusted for:
- Possession of shared webhook secret
- Endpoint ownership (HTTPS endpoint)
- Idempotent processing of duplicate deliveries
Not trusted for:
- Bypassing signature verification
- Modifying webhook payloads
- Skipping deduplication checks
Trusted for:
- Secret rotation and management
- Incident response and diagnostics
- Delivery monitoring and alerting
Not trusted for:
- Reading secrets from logs
- Bypassing audit trails
- Silently dropping verified deliveries
Trusted for:
- Constructing signed payloads
- Retry scheduling and execution
- Durable delivery state management
Not trusted for:
- Mutating verified deliveries
- Dropping failed deliveries without logging
- Bypassing retry policy
| Condition | Expected Result | HTTP Status | Retry? |
|---|---|---|---|
| Network timeout | Log error, schedule retry | N/A | Yes |
| Connection refused | Log error, schedule retry | N/A | Yes |
| 408 Request Timeout | Log error, schedule retry | 408 | Yes |
| 429 Too Many Requests | Log error, schedule retry | 429 | Yes |
| 500 Internal Server Error | Log error, schedule retry | 500 | Yes |
| 502 Bad Gateway | Log error, schedule retry | 502 | Yes |
| 503 Service Unavailable | Log error, schedule retry | 503 | Yes |
| 504 Gateway Timeout | Log error, schedule retry | 504 | Yes |
| 400 Bad Request | Log error, mark permanent failure | 400 | No |
| 401 Unauthorized | Log error, mark permanent failure | 401 | No |
| 403 Forbidden | Log error, mark permanent failure | 403 | No |
| 404 Not Found | Log error, mark permanent failure | 404 | No |
| 200 OK | Mark delivered, log success | 200 | No |
| 201 Created | Mark delivered, log success | 201 | No |
| 204 No Content | Mark delivered, log success | 204 | No |
| Max attempts exceeded | Mark permanent failure, log error | N/A | No |
- queued - Added to outbox, waiting for processing
- pending - Delivery in progress or waiting for retry
- delivered - Successfully delivered (2xx response)
- failed - Delivery failed but may be retried
- permanent_failure - Delivery failed and moved to DLQ
queued
|- pending (when processed by outbox worker)
pending
|- delivered (on 2xx response)
|- pending (on retryable error, schedule next retry)
|- permanent_failure (on non-retryable error or max attempts exceeded)
permanent_failure
|- queued (manual retry from DLQ)
POST /api/webhooks/queue
Content-Type: application/json
{
"event": {
"id": "event_123",
"type": "stream.created",
"timestamp": 1710000000000,
"data": { "streamId": "stream_123" }
},
"endpointUrl": "https://consumer.example.com/webhook",
"secret": "webhook_secret_123"
}
GET /api/webhooks/deliveries/:deliveryId
Response:
{
"id": "delivery_123",
"deliveryId": "deliv_123",
"eventId": "event_123",
"eventType": "stream.created",
"status": "pending",
"attempts": [
{
"attemptNumber": 1,
"timestamp": "2024-03-10T12:00:00Z",
"statusCode": 503,
"error": null,
"nextRetryAt": "2024-03-10T12:00:01Z"
}
],
"createdAt": "2024-03-10T12:00:00Z",
"updatedAt": "2024-03-10T12:00:00Z"
}GET /api/webhooks/deliveries
Response:
{
"total": 42,
"deliveries": [
{
"id": "delivery_123",
"deliveryId": "deliv_123",
"eventId": "event_123",
"eventType": "stream.created",
"status": "pending",
"attemptCount": 1,
"createdAt": "2024-03-10T12:00:00Z",
"updatedAt": "2024-03-10T12:00:00Z"
}
]
}POST /api/webhooks/verify?secret=webhook_secret_123
Content-Type: application/json
x-fluxora-delivery-id: deliv_123
x-fluxora-timestamp: 1710000000
x-fluxora-signature: <hex-encoded-signature>
{"event": "stream.created", "data": {...}}
POST /internal/webhooks/process-outbox?secret=webhook_secret_123
This endpoint should be called periodically (e.g., every 5 seconds) by a background job to process outbox items.
POST /internal/webhooks/retry?secret=webhook_secret_123
This endpoint should be called periodically (e.g., every 10 seconds) by a background job to process pending retries.
GET /api/webhooks/outbox?priority=high&status=ready
Response:
{
"total": 15,
"items": [
{
"id": "outbox_123",
"deliveryId": "deliv_123",
"eventId": "event_123",
"eventType": "stream.created",
"endpointUrl": "https://consumer.example.com/webhook",
"priority": "high",
"attempts": 0,
"maxAttempts": 5,
"scheduledFor": "2024-03-10T12:00:00Z",
"createdAt": "2024-03-10T12:00:00Z"
}
]
}GET /api/webhooks/dlq?limit=50
Response:
{
"total": 3,
"items": [
{
"id": "dlq_123",
"deliveryId": "deliv_123",
"eventId": "event_123",
"eventType": "stream.created",
"endpointUrl": "https://consumer.example.com/webhook",
"failureReason": "Max attempts exceeded",
"attemptCount": 5,
"createdAt": "2024-03-10T12:00:00Z",
"processedAt": null
}
]
}POST /api/webhooks/dlq/dlq_123/retry
Content-Type: application/json
{
"secret": "webhook_secret_123"
}
GET /api/webhooks/circuit-breakers
Response:
{
"total": 2,
"states": [
{
"endpointUrl": "https://consumer.example.com/webhook",
"state": "open",
"failureCount": 15,
"lastFailureTime": "2024-03-10T12:00:00Z",
"nextAttemptTime": "2024-03-10T12:05:00Z"
}
]
}GET /api/webhooks/metrics
Response:
{
"totalDeliveries": 1000,
"successfulDeliveries": 950,
"failedDeliveries": 30,
"dlqItems": 20,
"outboxItems": 5,
"successRate": 95.0,
"failureRate": 5.0
}All webhook deliveries include these headers:
| Header | Value | Purpose |
|---|---|---|
x-fluxora-delivery-id |
UUID | Deduplication and tracking |
x-fluxora-timestamp |
Unix seconds | Replay attack prevention |
x-fluxora-signature |
HMAC-SHA256 hex | Payload integrity verification |
x-fluxora-event |
Event type | Event classification |
Content-Type |
application/json | Payload format |
Consumers should verify webhook signatures using the canonical algorithm:
import { verifyWebhookSignature } from './src/webhooks/signature.js';
const verification = verifyWebhookSignature({
secret: process.env.FLUXORA_WEBHOOK_SECRET,
deliveryId: req.header('x-fluxora-delivery-id'),
timestamp: req.header('x-fluxora-timestamp'),
signature: req.header('x-fluxora-signature'),
rawBody: req.rawBody,
isDuplicateDelivery: (deliveryId) => seenDeliveryIds.has(deliveryId),
});
if (!verification.ok) {
return res.status(verification.status).json({
error: verification.code,
message: verification.message,
});
}
// Process webhookGET /health- Service health and indexer statusGET /api/webhooks/deliveries- Webhook delivery queue status
Operators should monitor:
- Delivery success rate - Percentage of webhooks delivered successfully
- Retry rate - Percentage of webhooks requiring retries
- Permanent failure rate - Percentage of webhooks that failed permanently
- Average delivery time - Time from event to successful delivery
- Queue depth - Number of pending deliveries
When webhook deliveries are failing:
- Check
/healthendpoint for service status - Query
/api/webhooks/deliveriesto see pending deliveries - Inspect logs for delivery attempt details and errors
- Verify consumer endpoint is accessible and responding
- Check consumer logs for webhook processing errors
- Verify webhook secret is correct on both sides
- Check network connectivity between services
- Review retry policy configuration
All webhook operations are logged with:
- Delivery ID for correlation
- Event ID and type
- Attempt number and status code
- Error messages for failures
- Next retry time for pending deliveries
- Request correlation ID for tracing
Run webhook tests:
npm test -- src/webhooks/Coverage includes:
- Retry backoff calculation
- Status code classification
- Delivery state transitions
- Deduplication logic
- Signature verification
Run full integration tests:
npm test -- tests/webhooks.test.tsCoverage includes:
- End-to-end webhook delivery
- Retry behavior under various failure modes
- Header validation
- Deduplication across attempts
Test webhook delivery locally:
# Start the server
npm run dev
# Queue a webhook delivery
curl -X POST http://localhost:3000/api/webhooks/queue \
-H "Content-Type: application/json" \
-d '{
"event": {
"id": "event_test",
"type": "stream.created",
"timestamp": '$(date +%s)'000,
"data": {"streamId": "stream_test"}
},
"endpointUrl": "https://webhook.site/unique-id",
"secret": "test_secret"
}'
# Check delivery status
curl http://localhost:3000/api/webhooks/deliveries- Database persistence - Currently uses in-memory store; production should use database
- Webhook subscriptions - No consumer endpoint registration yet
- Circuit breaker - No automatic disabling of failing endpoints
- Rate limiting per endpoint - No per-consumer rate limits
- Webhook secret rotation - No automated secret rotation
- Dead-letter queue - No separate handling for permanently failed deliveries
- Add database-backed webhook delivery store
- Implement webhook subscription management
- Add circuit breaker for failing endpoints
- Implement per-endpoint rate limiting
- Add webhook secret rotation mechanism
- Create dead-letter queue for failed deliveries
- Add webhook delivery metrics and dashboards
- Implement webhook replay functionality