Retry logic for push notification delivery following Apple and Google best practices.
Retry Strategy
Based on official Apple APNS and Google FCM documentation:
- Connection errors: Retry with exponential backoff (10s, 20s, 40s)
- Server errors (5xx): Retry with exponential backoff
- Rate limited (429): Respect
retry-afterheader, or default to 60 seconds - Permanent failures: Do not retry (bad token, payload too large, etc.)
Configuration
config :pushx,
retry_enabled: true,
retry_max_attempts: 3,
retry_base_delay_ms: 10_000, # 10 seconds (Google recommends minimum 10s)
retry_max_delay_ms: 60_000 # 60 seconds maxReferences
Summary
Functions
Calculates the delay before the next retry attempt.
Returns true if the error is retryable.
Executes a function with retry logic.
Functions
@spec calculate_delay(PushX.Response.t(), pos_integer(), pos_integer(), pos_integer()) :: pos_integer()
Calculates the delay before the next retry attempt.
- For rate limiting: Uses retry_after value or 60 seconds default
- For other errors: Exponential backoff with jitter
Exponential Backoff Formula
delay = min(base_delay * 2^(attempt-1) + jitter, max_delay)
@spec retryable?(PushX.Response.t()) :: boolean()
Returns true if the error is retryable.
Retryable errors:
:connection_error- Network/connection failure:rate_limited- Too many requests (with backoff):server_error- Provider server error (5xx)
Non-retryable (permanent) errors:
:invalid_token- Device token is invalid:expired_token- Device token has expired:unregistered- Device is no longer registered:payload_too_large- Payload exceeds size limit:unknown_error- Unrecognized error (could be client-side issue)
@spec with_retry( (-> {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}), keyword() ) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Executes a function with retry logic.
The function should return {:ok, response} or {:error, response}.
Retries are only attempted for retryable errors.
Options
:max_attempts- Maximum number of attempts (default: 3):base_delay_ms- Base delay in milliseconds (default: 10_000):max_delay_ms- Maximum delay in milliseconds (default: 60_000)
Examples
PushX.Retry.with_retry(fn -> PushX.APNS.send_once(token, payload, opts) end)