Skip to main content

Retry strategies

Reqon provides built-in retry handling for transient failures with configurable backoff strategies.

Basic retry configuration

get "/data" {
retry: {
maxAttempts: 3,
backoff: exponential,
initialDelay: 1000,
maxDelay: 30000
}
}

Retry options

OptionDescriptionDefault
maxAttemptsMaximum number of retry attempts3
backoffBackoff strategyexponential
initialDelayFirst retry delay (ms)1000
maxDelayMaximum delay between retries (ms)30000

Backoff strategies

Exponential backoff

Doubles the delay after each attempt:

get "/data" {
retry: {
maxAttempts: 5,
backoff: exponential,
initialDelay: 1000
}
}

Timing:

  • Attempt 1: immediate
  • Attempt 2: wait 1000ms
  • Attempt 3: wait 2000ms
  • Attempt 4: wait 4000ms
  • Attempt 5: wait 8000ms

Linear backoff

Adds a fixed delay each time:

get "/data" {
retry: {
maxAttempts: 5,
backoff: linear,
initialDelay: 2000
}
}

Timing:

  • Attempt 1: immediate
  • Attempt 2: wait 2000ms
  • Attempt 3: wait 4000ms
  • Attempt 4: wait 6000ms
  • Attempt 5: wait 8000ms

Constant backoff

Same delay every time:

get "/data" {
retry: {
maxAttempts: 5,
backoff: constant,
initialDelay: 5000
}
}

Timing:

  • Attempt 1: immediate
  • Attempt 2: wait 5000ms
  • Attempt 3: wait 5000ms
  • Attempt 4: wait 5000ms
  • Attempt 5: wait 5000ms

Maximum delay

Cap the maximum delay:

get "/data" {
retry: {
maxAttempts: 10,
backoff: exponential,
initialDelay: 1000,
maxDelay: 30000 // Cap at 30 seconds
}
}

Without maxDelay, exponential backoff would reach:

  • Attempt 8: 128 seconds
  • Attempt 9: 256 seconds
  • Attempt 10: 512 seconds

With maxDelay: 30000, all delays are capped at 30 seconds.

Conditional retry

Use match for conditional retry logic:

action FetchWithConditionalRetry {
get "/data"

match response {
// Retry on rate limit
{ error: _, code: 429 } -> retry {
maxAttempts: 5,
backoff: exponential,
initialDelay: 60000 // Start with 1 minute
},

// Retry on server errors
{ error: _, code: 500 } -> retry {
maxAttempts: 3,
backoff: exponential,
initialDelay: 5000
},

// Retry on timeout
{ error: "timeout" } -> retry {
maxAttempts: 3,
backoff: constant,
initialDelay: 10000
},

// Don't retry client errors
{ error: _, code: 400 } -> abort "Bad request",
{ error: _, code: 401 } -> abort "Unauthorized",
{ error: _, code: 404 } -> skip,

// Success
_ -> continue
}
}

Retry after header

Reqon respects the Retry-After header when present:

get "/rate-limited-api" {
retry: {
maxAttempts: 5,
backoff: exponential,
initialDelay: 1000
}
}

If the API returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Reqon waits 60 seconds before retrying, regardless of backoff settings.

Combining with other features

With pagination

get "/items" {
paginate: offset(offset, 100),
until: length(response) == 0,
retry: {
maxAttempts: 3,
backoff: exponential
}
}

Each page request uses retry logic independently.

With rate limiting

source API {
auth: bearer,
base: "https://api.example.com",
rateLimit: {
requestsPerMinute: 60,
strategy: "pause"
}
}

action Fetch {
get "/items" {
retry: {
maxAttempts: 3,
backoff: exponential
}
}
}

Rate limiting runs before retry; retry handles unexpected failures.

Jump and retry

For complex retry scenarios like token refresh:

action FetchData {
get "/protected-data"

match response {
{ error: _, code: 401 } -> jump RefreshToken then retry,
_ -> store response -> data { key: .id }
}
}

action RefreshToken {
post "/auth/refresh" {
body: { refreshToken: env("REFRESH_TOKEN") }
}

// Token is automatically used in subsequent requests
}

The jump RefreshToken then retry directive:

  1. Executes the RefreshToken action
  2. Retries the original request with the new token

Per-source retry configuration

Configure default retry at the source level:

source UnreliableAPI {
auth: bearer,
base: "https://flaky.api.com",
retry: {
maxAttempts: 5,
backoff: exponential,
initialDelay: 2000,
maxDelay: 60000
}
}

action Fetch {
// Uses source-level retry config
get "/data"
}

Request-level config overrides source-level:

action FetchWithOverride {
get "/data" {
retry: {
maxAttempts: 10 // Override just maxAttempts
}
}
}

Best practices

Use exponential backoff for APIs

get "/api/data" {
retry: {
maxAttempts: 5,
backoff: exponential,
initialDelay: 1000,
maxDelay: 30000
}
}

Handle specific error codes

match response {
// Transient errors - retry
{ code: 429 } -> retry { maxAttempts: 5 },
{ code: 503 } -> retry { maxAttempts: 3 },
{ code: 504 } -> retry { maxAttempts: 3 },

// Permanent errors - don't retry
{ code: 400 } -> abort "Bad request",
{ code: 401 } -> abort "Unauthorized",
{ code: 403 } -> abort "Forbidden",
{ code: 404 } -> skip,

_ -> continue
}

Set reasonable limits

// Good: reasonable limits
retry: {
maxAttempts: 5,
maxDelay: 60000
}

// Risky: too aggressive
retry: {
maxAttempts: 100,
maxDelay: 1000 // 1 second
}

// Risky: too long
retry: {
maxAttempts: 20,
initialDelay: 60000 // 1 minute start
}

Log retry attempts

Combine with match for observability:

action FetchWithLogging {
get "/data" {
retry: {
maxAttempts: 3,
backoff: exponential
}
}

match response {
{ error: e } -> {
store {
endpoint: "/data",
error: e,
timestamp: now()
} -> retryLogs
abort e
},
_ -> continue
}
}

Troubleshooting

Retry not working

Ensure the response matches retry conditions:

// Retry only triggers on match directive
match response {
{ error: _ } -> retry { maxAttempts: 3 }, // This triggers retry
_ -> continue
}

Too many retries

Add a maximum delay:

retry: {
maxAttempts: 10,
backoff: exponential,
initialDelay: 1000,
maxDelay: 30000 // Cap delays
}

Retry after token refresh

Use jump then retry:

match response {
{ code: 401 } -> jump RefreshToken then retry,
_ -> continue
}