Skip to content

Redis Lists (work queue)

A list is a competing-consumers queue: a producer LPUSHes, consumers pop from the right, and each entry goes to exactly one consumer (no fan-out, no replay). Simple mode is at-most-once (BRPOP, no ack):

// Simple at-most-once work queue: BRPOP, no ack.
#[subscriber(RedisList::new("jobs"))]
async fn run_job(job: &Job) -> HandlerResult {
    println!("running job {}", job.id);
    HandlerResult::Ack
}

Reliable mode moves each entry to a processing list and removes it on ack (at-least-once), so a crashed handler does not silently lose its job:

// Reliable at-least-once work queue: the job moves to a processing list and is removed on ack.
#[subscriber(RedisList::new("jobs.reliable").reliable())]
async fn run_reliable_job(job: &Job) -> HandlerResult {
    println!("running reliable job {}", job.id);
    HandlerResult::Ack
}

Publish with broker.list_publisher() (LPUSH). Headers travel in the same frame as Pub/Sub: a lossless binary frame by default, or a readable codec-serialized envelope when a codec is set on both ends (.codec(JsonCodec)).

Orphan recovery

A dead consumer can strand a reliable entry on its processing list, since Redis lists have no native pending tracking. Opt into a recovery watchdog by naming a ZSET key (off by default):

// Reliable mode with orphan recovery: a dead consumer's in-flight job is returned to the queue. The
// recovery ZSET key is named explicitly, and min_idle must exceed the longest legitimate handler
// runtime (set it too low and a still-running job gets recovered and processed twice).
#[subscriber(
    RedisList::new("jobs.recoverable")
        .reliable()
        .min_idle(Duration::from_secs(30))
        .recovery_zset("jobs.recoverable.inflight")
)]
async fn run_recoverable_job(job: &Job) -> HandlerResult {
    println!("running recoverable job {}", job.id);
    HandlerResult::Ack
}

Each claim is recorded in the ZSET (score = claim time); a sweeper folded into the subscription's read loop returns entries idle past min_idle to the main list, where a live consumer re-claims them. Like the Streams reclaim path, min_idle must exceed the longest legitimate handler runtime, or a still-running entry is recovered and processed twice. An optional recovery_ttl cleans up an abandoned ZSET key but must exceed min_idle. Without recovery, Redis Streams remain the recommended durable, recoverable path.

List publisher TTL

An idle list can be bounded with a key TTL: broker.list_publisher().ttl(Duration::from_secs(300)) re-arms a PEXPIRE on the list key on every publish, so an actively used queue never expires and only an idle one lapses. It is off by default and per-key (the whole list), not per-entry - Redis lists have no per-element expiry. Pub/Sub has no equivalent (PUBLISH stores nothing to expire), and streams bound their size with trimming rather than a TTL.