Exponential Backoff
Increasing delays between retries
Smart Retry Logic
Retry only transient failures
Durable Steps
Cache completed steps across retries
Failure Monitoring
Track and alert on failures
Retry Configuration
Configure how tasks should be retried when they fail. By default, tasks are retried up to 3 times with exponential backoff. Set the retry option on your task definition:
import { task } from '@sylphx/sdk/tasks'
export const processOrder = task({
id: 'process-order',
retry: {
maxAttempts: 5,
backoff: 'exponential',
},
run: async ({ orderId }) => {
await chargeAndFulfill(orderId)
},
})Retry Options
| Property | Type | Description |
|---|---|---|
maxAttempts | number= 3 | Maximum number of retry attempts (1-10) |
backoff | "fixed" | "exponential"= "exponential" | Backoff strategy between retries |
Exponential Backoff
Exponential backoff increases the delay between retries, giving failing services time to recover while avoiding overwhelming them with requests.
import { task } from '@sylphx/sdk/tasks'
// Best for most use cases — prevents thundering herd
export const sendWebhook = task({
id: 'send-webhook',
retry: {
maxAttempts: 5,
backoff: 'exponential',
},
run: async ({ url, event }) => {
await fetch(url, {
method: 'POST',
body: JSON.stringify({ event }),
})
},
})Jitter
Durable Steps — Skip Completed Work
The most powerful retry pattern is using step.run() inside your task. Completed steps are cached and skipped on retry — only the failed step reruns.
import { task } from '@sylphx/sdk/tasks'
export const processOrder = task({
id: 'process-order',
retry: { maxAttempts: 5, backoff: 'exponential' },
run: async ({ orderId }, { step, attempt }) => {
// If this task retries, completed steps are skipped
const order = await step.run('fetch-order', () => getOrder(orderId))
const reserved = await step.run('reserve-inventory', () =>
reserveInventory(order.items)
)
await step.run('charge-payment', () =>
chargePayment(order.total, reserved.reservationId)
)
await step.run('send-confirmation', () =>
sendEmail({ to: order.email, template: 'order-confirmed' })
)
},
})
// If 'charge-payment' fails:
// Retry #2: fetch-order ✓ (cached), reserve-inventory ✓ (cached), charge-payment → retried
// No duplicate emails, no double-reservationsHandling Transient vs Permanent Failures
Distinguish between transient failures (retry) and permanent failures (don't retry) inside your task handler:
import { task } from '@sylphx/sdk/tasks'
export const processOrder = task({
id: 'process-order',
retry: { maxAttempts: 5, backoff: 'exponential' },
run: async ({ orderId }, { step }) => {
const order = await step.run('fetch-order', () => getOrder(orderId))
// Permanent failure — throw a non-retriable error
if (!order) {
throw Object.assign(new Error('Order not found'), { permanent: true })
}
if (order.status === 'cancelled') {
throw Object.assign(new Error('Order is cancelled'), { permanent: true })
}
// Transient failure — just throw, task will retry
await step.run('charge', () => chargePayment(order))
},
})Throw Errors for Transient Failures
permanent: true.Best Practices
Make Tasks Idempotent
Tasks may run multiple times due to retries. Use step.run() and idempotency keys to prevent duplicate processing.
Use Durable Steps
Wrap side effects in step.run() so they're cached and not repeated on retry.
Set Appropriate Timeouts
Configure task timeouts to prevent stuck runs from blocking retries.
Monitor Failures
Use the Sylphx console to monitor task failure rates and identify systemic issues.
Idempotency Pattern
import { task } from '@sylphx/sdk/tasks'
export const processOrder = task({
id: 'process-order',
run: async ({ orderId }, { step }) => {
// Check if already processed (idempotency guard)
const existing = await step.run('check-existing', () =>
db.processedOrders.findUnique({ where: { orderId } })
)
if (existing) {
// Already processed — return cached result
return { alreadyProcessed: true, result: existing }
}
// Process the order
const result = await step.run('process', () => processOrder(orderId))
// Record completion
await step.run('record', () =>
db.processedOrders.create({ data: { orderId, processedAt: new Date() } })
)
return result
},
})