Understanding Retry Behavior
When a webhook delivery fails, QueueBear automatically retries with exponential backoff:
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1 (initial) | - | 0s |
| 2 (retry 1) | 1 second | 1s |
| 3 (retry 2) | 2 seconds | 3s |
| 4 (retry 3) | 4 seconds | 7s |
| 5 (retry 4) | 8 seconds | 15s |
| 6 (retry 5) | 16 seconds | 31s |
With the default 3 retries (4 total attempts), delivery spans ~7 seconds. With maximum 5 retries, ~31 seconds.
The Dead Letter Queue (DLQ)
Messages that fail all retry attempts are automatically moved to the Dead Letter Queue. This is your safety net for prolonged downtime.
Monitoring the DLQ
const { entries } = await qb.dlq.list();
for (const entry of entries) {
console.log({
id: entry.id,
destination: entry.destination,
failureReason: entry.failureReason,
totalAttempts: entry.totalAttempts,
lastAttemptAt: entry.lastAttemptAt,
});
}
Setting Up DLQ Alerts
// Check DLQ every 5 minutes
await qb.schedules.create({
destination: "https://app.com/api/admin/dlq-monitor",
cron: "*/5 * * * *",
body: JSON.stringify({ action: "check_and_alert" }),
});
Recovering from DLQ
// Retry a single entry
const result = await qb.dlq.retry(dlqId);
// Retry all entries
const results = await qb.dlq.retryAll();
// Selective retry
const { entries } = await qb.dlq.list();
for (const entry of entries) {
if (entry.destination.includes("payment-service")) {
await qb.dlq.retry(entry.id);
}
}
Using Events for External Dependencies
When your workflow depends on a service that may be down, use waitForEvent():
export const POST = serve<OrderInput>(async (ctx) => {
await ctx.run("create-order", async () => { /* ... */ });
// Wait for external service to signal readiness
const externalResult = await ctx.waitForEvent(
"wait-for-external-service",
"external.service.ready",
{
eventKey: ctx.input.orderId,
timeoutSeconds: 4 * 60 * 60, // Wait up to 4 hours
}
);
// Now safe to proceed
await ctx.run("process-payment", async () => {
await paymentService.charge(ctx.input.orderId, ctx.input.amount);
});
return { success: true };
});
When the service recovers, send the event:
await qb.workflows.sendEvent("external.service.ready", {
eventKey: orderId,
payload: { recoveredAt: new Date().toISOString() },
});
Workflow Checkpoint and Resume
Workflows automatically checkpoint after each completed step. On retry, they resume from the last checkpoint.
export const POST = serve<Input>(async (ctx) => {
// Step 1: Completes, result cached
const user = await ctx.run("fetch-user", async () => {
return await userService.get(ctx.input.userId);
});
// Step 2: Fails due to service outage
await ctx.run("update-crm", async () => {
await crmService.update(user); // Service is down!
});
// Step 3: Never reached
await ctx.run("send-notification", async () => { ... });
});
When the CRM service recovers:
// Retry the workflow
await qb.workflows.retry(runId);
// Step 1 returns cached result (NOT re-executed)
// Step 2 executes (CRM service now available)
// Step 3 executes
Graceful Degradation Strategies
Skip Non-Critical Steps
export const POST = serve<OrderInput>(async (ctx) => {
// Critical: Process order
await ctx.run("process-order", async () => {
await orderService.process(ctx.input);
});
// Non-critical: Update analytics (skip if fails)
try {
await ctx.run("update-analytics", async () => {
await analyticsService.track(ctx.input);
});
} catch (error) {
console.warn("Analytics update failed, continuing:", error);
}
return { success: true };
});
Fallback Destinations
await ctx.run("process-with-fallback", async () => {
try {
return await primaryService.process(data);
} catch (error) {
console.warn("Primary service failed, trying fallback");
return await fallbackService.process(data);
}
});
Example: Payment Service Outage Recovery
Scenario: Your payment provider experiences a 4-hour outage.
During Outage
- Orders triggered will complete step 1 (create order)
- Fail at step 2 (payment) after retries
- Workflow status becomes "failed"
- Messages go to DLQ
Recovery
// 1. Check health
const isHealthy = await paymentService.healthCheck();
if (isHealthy) {
// 2. Retry all DLQ entries
await qb.dlq.retryAll();
// 3. Retry failed workflows
const failedWorkflows = await db.workflowRuns.findMany({
where: { status: "failed", workflowName: "order-processing" },
});
for (const workflow of failedWorkflows) {
await qb.workflows.retry(workflow.id);
}
}
Summary
| Strategy | Use Case |
|---|---|
| Retry configuration | Transient failures (seconds) |
| DLQ monitoring | Extended outages (minutes to hours) |
| Event-based waiting | Known dependencies with uncertain recovery |
| Workflow retry | Resume failed workflows after recovery |
| Graceful degradation | Non-critical functionality |