ESC
Type to search...

Background Jobs

Persistent job queue backed by PostgreSQL

Background jobs let you defer work to run outside the request cycle. Sending emails, processing uploads, generating reports — anything that shouldn't block an HTTP response.

Rapina's job system uses your existing PostgreSQL database as the queue. No Redis, no RabbitMQ, no extra infrastructure. Jobs are rows in a rapina_jobs table, claimed by workers with FOR UPDATE SKIP LOCKED for safe concurrent processing.

This page covers the foundation: the database table, the types, and the CLI setup. The #[job] macro, Jobs extractor, and worker runtime are coming in future releases.

Prerequisites

You need the database feature with PostgreSQL. The jobs migration uses PostgreSQL-specific features (gen_random_uuid(), partial indexes) and does not support MySQL or SQLite.

[dependencies]
rapina = { version = "0.10", features = ["postgres"] }

You also need a database connection configured in your app — see the Database page.

Setup

Run the CLI command from your project root:

rapina jobs init

This adds the framework's create_rapina_jobs migration to your src/migrations/mod.rs. If the file doesn't exist yet, it creates one. If the migration is already configured, it skips silently.

The result looks like this:

use rapina::jobs::create_rapina_jobs;

mod m20260315_000001_create_users;

rapina::migrations! {
    create_rapina_jobs,
    m20260315_000001_create_users,
}

The framework migration uses a zero timestamp (m00000000_000000_) so it always sorts before your application migrations, regardless of their dates.

Next time your app starts and runs migrations, the rapina_jobs table will be created.

Table Schema

The migration creates a rapina_jobs table with the following columns:

ColumnTypeDefaultDescription
idUUIDgen_random_uuid()Primary key
queueVARCHAR(255)'default'Logical queue name
job_typeVARCHAR(255)Fully-qualified type name for dispatch
payloadJSONB'{}'Arbitrary data passed to the handler
statusVARCHAR(32)'pending'Lifecycle state
attemptsINTEGER0Number of times this job has been attempted
max_retriesINTEGER3Maximum retry count before permanent failure
run_atTIMESTAMPTZnow()Earliest time to execute
started_atTIMESTAMPTZNULLWhen a worker started processing
locked_untilTIMESTAMPTZNULLLease expiry for crash recovery
finished_atTIMESTAMPTZNULLWhen the job completed or permanently failed
last_errorTEXTNULLError from the most recent failed attempt
trace_idVARCHAR(64)NULLDistributed trace ID from the enqueuing request
created_atTIMESTAMPTZnow()Insertion timestamp

A partial index on (queue, run_at) WHERE status = 'pending' optimizes the worker's claim query.

Types

JobStatus

The JobStatus enum represents the lifecycle of a job:

use rapina::prelude::*;

// Available when the `database` feature is enabled
let status = JobStatus::Pending;
println!("{status}"); // "pending"

let parsed: JobStatus = "running".parse().unwrap();
VariantMeaning
PendingQueued and waiting for a worker
RunningClaimed by a worker, currently executing
CompletedFinished successfully
FailedExhausted all retries or hit a fatal error

JobStatus implements Display, FromStr, Serialize, Deserialize, Hash, Copy, and Eq. The string representation is always lowercase.

JobRow

JobRow is a plain struct that maps directly to a row in the rapina_jobs table. It derives SeaORM's FromQueryResult so you can use it with raw queries:

use rapina::jobs::JobRow;
use rapina::sea_orm::{FromQueryResult, Statement, DatabaseBackend};
use rapina::database::Db;

let rows: Vec<JobRow> = JobRow::find_by_statement(
    Statement::from_string(
        DatabaseBackend::Postgres,
        "SELECT * FROM rapina_jobs WHERE queue = 'emails' AND status = 'failed'"
    )
)
.all(db.conn())
.await
.map_err(DbError::from)?;

for row in &rows {
    let status = row.parse_status().unwrap();
    println!("{}: {} (attempts: {})", row.id, status, row.attempts);
}

The status field is a String because SeaORM's FromQueryResult derive doesn't support custom enum deserialization. Use parse_status() to get a typed JobStatus.

Manual Setup

If you prefer not to use the CLI, add the migration reference manually:

// src/migrations/mod.rs
use rapina::jobs::create_rapina_jobs;

mod m20260315_000001_create_users;

rapina::migrations! {
    create_rapina_jobs,
    m20260315_000001_create_users,
}

The create_rapina_jobs module is exported from the rapina crate, so there's no file to create in your project — just the use import and the macro entry.