Cancel a job when it should never run again, or request cancellation of an execution already in progress. A cancelled job is not retried automatically.
Cancelling from within a worker
Ordinary worker errors are retried, on the assumption that the problem is temporary or can be fixed by a code deploy. When a worker detects a permanent failure, such as an account that has been closed, it can cancel the job instead.
Return river.JobCancel from the worker’s Work method:
func (w *CancellingWorker) Work(ctx context.Context, j *river.Job[CancellingArgs]) error { return river.JobCancel(fmt.Errorf("account has been closed"))}The wrapped error is persisted in the job’s error history, so operators can see why it was cancelled. See the JobCancel example for complete code.
Raise the exception returned by River.job_cancel from the worker’s work method:
class CancellingWorker def self.kind = "cancelling"
def work(job) raise River.job_cancel("account has been closed") endendThe message is persisted in the job’s error history. River.job_cancel also accepts an exception, preserving its message and retaining the original exception as its cause. Raise the result: returning the exception object alone does not cancel the job.
Return Ok(WorkOutcome::Cancel) from the worker’s work method. An ordinary Err follows the retry policy instead.
use riverqueue::{Job, WorkContext, WorkOutcome, Worker};
impl Worker<CancellingArgs> for CancellingWorker { type Error = std::convert::Infallible;
async fn work( &self, _context: WorkContext, _job: Job<CancellingArgs>, ) -> Result<WorkOutcome, Self::Error> { Ok(WorkOutcome::Cancel) }}This records “job cancelled by worker” in the job’s error history. WorkOutcome::Cancel does not take a custom error message; log any additional context before returning it.
Return cancel({ reason }) from the worker function registered with Workers.add:
import { cancel, defineJob, Workers } from "riverqueue";
const cancellingJob = defineJob({ kind: "cancelling" });
const workers = new Workers().add(cancellingJob, async () => { return cancel({ reason: "account has been closed" });});The optional reason is persisted in the job’s error history as JobCancelError: <reason>. Return the outcome to cancel the job; throwing an ordinary error or rejecting the worker’s promise follows the retry policy instead.
Cancelling a job from the client
Use a job’s ID to cancel it from a client, whether or not it is currently running. For an existing job, each method returns its current record; a running job may still be marked running while its worker responds to the request.
Call Client.JobCancel:
if _, err := riverClient.JobCancel(ctx, jobID); err != nil { // handle error}See the JobCancelFromClient example for complete code.
Call client.job_cancel(job_id):
job = client.job_cancel(job_id)This raises River::NotFoundError if the job does not exist. Database errors also propagate to the caller.
Call client.jobs().cancel(job_id) and await the request:
let job = client.jobs().cancel(job_id).await?;The request does not execute until it is awaited. Handle the returned error or propagate it with ?.
Call client.jobs.cancel(jobID) and await the result. Job IDs are bigint values:
const job = await client.jobs.cancel(jobID);This returns null if the job does not exist. Database errors reject the promise; handle them with try/catch or let them propagate to the caller.
The result of cancellation depends on the job’s state and whether its worker finishes before the request takes effect. Cancellation does not undo work or external side effects that have already happened.
Jobs still running
Running jobs are marked for cancellation rather than immediately finalized. The client executing the job then signals or interrupts its worker. A worker can finish before this happens, and how it responds to the request depends on its language.
River cancels the worker’s context.Context. Go cannot interrupt a running goroutine, so the worker must respect context cancellation and return promptly. Pass ctx to database and HTTP calls, and check ctx.Done() in long-running loops or waits.
After a remote cancellation signal is received, returning an error cancels the job permanently. Returning nil still completes the job successfully.
If the worker finishes after the cancellation request is recorded but before it receives the signal:
- If the job was due to be finalized because it completed successfully, was cancelled from within, or was discarded due to exceeding its max attempts, the job will be updated as usual.
- If the job was snoozed to run again later or encountered a retryable error, the job will be marked as
cancelledand will not be attempted again.
River checks running jobs for cancellation requests and raises River::JobCancelError in the worker thread when it finds one. Let this exception propagate to River. If you rescue StandardError inside a worker, re-raise River::JobCancelError instead of swallowing it or retrying the operation yourself.
Use ensure blocks or block-based resource management to release resources when work is interrupted. An uninterruptible native call can delay the worker’s response.
River also checks for cancellation before recording a successful completion, so a worker that returns normally can still be marked cancelled if the request is observed at that point. A job whose completion has already been recorded remains completed.
River signals WorkContext::cancellation_token(). Use the token’s cancelled() future with tokio::select! to stop long-running work and return promptly. The token is also used for timeouts and client shutdown, so a signalled token alone does not mean the job should be permanently cancelled. Return a riverqueue::WorkCancelled error to stop cooperatively and let River determine the outcome.
After a remote cancellation signal is received, returning an error cancels the job permanently. Returning Ok(WorkOutcome::Complete) still completes it successfully. A worker that ignores cancellation may eventually have its task aborted after the client’s stuck-job threshold.
If the worker finishes before it receives the signal, a recorded cancellation request prevents retry or snooze transitions from scheduling another attempt. A transition to a finalized state can still win the race.
River aborts the worker’s signal, an AbortSignal, with a JobCancelledError. Cancellation is cooperative: pass signal to operations that support it, such as fetch, and check signal.throwIfAborted() in long-running loops. Use finally for cleanup and let abort errors propagate to River.
After a remote cancellation signal is received, throwing or rejecting cancels the job permanently. Returning snooze(...) or discard(...) also cancels it. A worker that still resolves successfully, including by returning complete(), completes the job as usual.
The same signal is used for job timeouts and client shutdown. Let River distinguish those cases instead of converting every abort into a permanent cancel() outcome. An in-process worker that ignores its signal cannot be forcibly interrupted; the optional @riverqueue/worker-threads runtime can terminate a worker thread after jobStuckThreshold.
If the worker finishes before receiving the signal, a recorded cancellation request prevents a retry or snooze from scheduling another attempt. A transition to a finalized state can still win the race.
Jobs still enqueued
For jobs that have not started (available, scheduled, retryable, or pending), cancellation is straightforward. The job is immediately and atomically marked as cancelled so that no client will fetch it from the queue and it will not be attempted again. If a worker claims the job first, the running-job behavior above applies instead.
Jobs already finalized
If the job is already finalized (cancelled, completed, or discarded), no changes are made.
Cancelling a job from the UI
The River UI enables cancelling jobs from the web interface. Cancellation can be performed in bulk from the job list page on selected jobs, or individually from the job details page.