PushX.Retry (PushX v0.5.0)

Copy Markdown View Source

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-after header, 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 max

References

Summary

Functions

Calculates the delay before the next retry attempt.

Returns true if the error is retryable.

Executes a function with retry logic.

Functions

calculate_delay(response, attempt, base_delay, max_delay)

@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)

retryable?(response)

@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)

with_retry(fun, opts \\ [])

@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)