When River jobs encounter an error or other failure, they are automatically retried after a delay. The default retry policy backs off for attempts ^ 4 + rand(±10%) seconds (0s, 1s, 16s, …), and jobs will be tried a maximum of 25 times by default.
Limiting retry attempts
The default limit is 25 total attempts, including the initial run. Set it to one to disable retries for ordinary worker errors. Insertion options override defaults on the job kind.
River’s default is to retry jobs up to 25 times as defined by river.DefaultMaxAttempts. This can be customized for a given worker using the optional JobArgsWithInsertOpts interface on the JobArgs implementation:
type RetryOnceJobArgs struct {}
func (args RetryOnceJobArgs) Kind() string { return "RetryOnceJob" }
func (args RetryOnceJobArgs) InsertOpts() river.InsertOpts { return river.InsertOpts{MaxAttempts: 1}}The max attempts can also be customized for an individual job at insertion time. This takes precedence over a job-level default:
_, err = riverClient.Insert(ctx, RetryOnceJobArgs{}, &river.InsertOpts{ // use a max attempts of 5 for this one job even though the job has a // default limit of 1: MaxAttempts: 5,})Set a default on argument objects or override it for one insertion:
class RetryOnceJobArgs def kind = "RetryOnceJob" def to_json = JSON.generate({}) def insert_opts = River::InsertOpts.new(max_attempts: 1)end
client.insert RetryOnceJobArgs.new, max_attempts: 5Set max_attempts on the args type for the default limit, or override it on a particular insertion with InsertOpts:
#[derive(Clone, Debug, serde::Deserialize, riverqueue::JobArgs, serde::Serialize)]#[river(kind = "RetryOnceJob", max_attempts = 1)]struct RetryOnceJobArgs {}
client.insert(RetryOnceJobArgs {}) .opts(riverqueue::InsertOpts::default().with_max_attempts(5)) .await?;Set maxAttempts in the job definition’s defaults, then override it in insertion options when an individual job needs a different limit:
const retryOnce = defineJob({ kind: "RetryOnceJob", defaults: { maxAttempts: 1 } });await client.insert(retryOnce, {}, { maxAttempts: 5 });Retry delays
Jobs are typically delayed for some amount of time between attempts. River provides reasonable default retry delays, but this behavior is also fully customizable at the client and worker level.
Client retry policy
The default retry policy uses a quartic backoff based on how many times the job has errored. A randomized ±10% jitter is applied to prevent stampeding errors from all retrying at the same time.
attempts ^ 4 + rand(±10%)A job which has errored repeatedly will see its retries delayed as shown below:
| Attempt | Delay (±10%) | |
|---|---|---|
| 0 → 1 | - | Initial run before an error (no delay) |
| 1 → 2 | 1s | Delay after first error |
| 2 → 3 | 16s | |
| 3 → 4 | 1m21s | |
| 4 → 5 | 4m16s | |
| 5 → 6 | 10m25s | |
| … | … | |
| 24 → 25 | 3d20h9m36s | Last retry, ~3 weeks after first run |
The last retry comes about three weeks after the first time a job is worked, so in case of a buggy job, there’s plenty of time to get a fix out before the job is finally discarded.
The client retry policy can easily be customized:
// LinearRetryPolicy delays subsequent retries by 5 seconds for each time// the job has failed (5s, 10s, 15s, etc.).type LinearRetryPolicy struct {}
// NextRetry returns the next retry time based on the non-generic JobRow// which includes an up-to-date Errors list.func (policy *LinearRetryPolicy) NextRetry(job *rivertype.JobRow) time.Time { // The latest error is not yet included in the job's Errors list, so we // add 1 to the length to account for that. return time.Now().Add(time.Duration(len(job.Errors) + 1) * 5 * time.Second)}
client, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ RetryPolicy: &LinearRetryPolicy{}, // ...})Provide a retry policy object whose next_retry method returns the next scheduled time. This example increases the delay by five seconds per recorded error:
class LinearRetryPolicy def next_retry(job, _error, now:) now + (job.errors.length + 1) * 5 endend
config = River::Config.new(retry_policy: LinearRetryPolicy.new)Implement RetryPolicy to return the delay until the next attempt, then attach it to the client builder:
struct LinearRetryPolicy;
impl riverqueue::RetryPolicy for LinearRetryPolicy { fn next_retry( &self, job: &riverqueue::JobRow, _error: &riverqueue::WorkError, _now: riverqueue::chrono::DateTime<riverqueue::chrono::Utc>, ) -> std::time::Duration { std::time::Duration::from_secs((job.errors.len() as u64 + 1) * 5) }}
let client = riverqueue::Client::builder(pool).retry_policy(LinearRetryPolicy).build()?;Supply a retryPolicy callback that returns the next retry time. This example adds five seconds of delay for each recorded error:
const client = new Client(driver, { retryPolicy: (job, now) => now.add({ seconds: (job.errors.length + 1) * 5 }),});Customizing retry delays for a specific worker
While the client retry policy applies to all kinds of jobs worked by that client, workers can also override the retry behavior for all jobs of that kind. This is done via the Worker interface’s NextRetry(*Job[T]) time.Time method. WorkerDefaults always returns 0 for this method to fallback to the client retry policy, so it can be customized for a given worker just by returning a non-zero value:
type ConstantRetryTimeWorker struct { river.WorkerDefaults[MyJobArgs]}
func (w *ConstantRetryTimeWorker) Work(job *Job[MyJobArgs]) error { // ...}
// NextRetry always schedules the next retry for 10 seconds from now.func (w *ConstantRetryTimeWorker) NextRetry(job *Job[MyJobArgs]) time.Time { return time.Now().Add(10*time.Second)}Define a worker’s next_retry method, returning an absolute time:
def next_retry(_job, _error) = Time.now.utc + 10Override next_retry in the worker’s Worker implementation. Return a delay, or None to use the client policy:
fn next_retry( &self, _job: &riverqueue::Job<MyArgs>, _error: &riverqueue::WorkError, _now: riverqueue::chrono::DateTime<riverqueue::chrono::Utc>,) -> Option<std::time::Duration> { Some(std::time::Duration::from_secs(10))}Set retryPolicy when registering a handler to override the client’s policy for that worker. Here, each retry is scheduled ten seconds later:
workers.add(myJob, handler, { retryPolicy: (_job, now) => now.add({ seconds: 10 }),});