AbsoluteJS

@absolutejs/queue-redis

@absolutejs/queue-redisv0.0.3betaData & Sync

Redis-backed JobStore for @absolutejs/queue. Atomic Lua claim, sorted-set scheduling by runAt, per-job hash records. For shops running Redis instead of (or alongside) Postgres.

#Installation

BASH
bun add @absolutejs/queue-redis

#Capabilities

Overview

Redis-backed JobStore for @absolutejs/queue. Sibling to @absolutejs/queue-postgres — same JobStore contract, different transport.

Docs: absolutejs.com/documentation/queue-overview#redis-adapter

When to use Redis vs Postgres

Concern — queue-redis — queue-postgres

Already have Redis — Win — New dep to operate

Already have Postgres — Skip — fewer deps — Win

Show 5 more

Durability after Redis crash — RDB snapshots + AOF (config-dependent) — WAL — point-in-time recovery

Throughput — Higher per-key (in-memory) — Lower (transactional)

Multi-region — Native cluster geo-replication — PG logical replication (heavier)

At-most-once semantics — Lease-based reap-on-expire — Same

Both adapters implement identical JobStore shape — swap them by config.

Atomic claim via Lua

claimDue runs a Lua script that atomically:

ZRANGEBYSCORE due 0 now LIMIT 0 N — find due jobs

ZREM due id for each — remove from the due set

Show 5 more

ZADD claimed (now+leaseMs) id for each — add to claimed with expiry

HSET status=claimed lockedAt=now lockedBy=worker

HGETALL for each — return the payloads

Without Lua, two concurrent workers could race the ZRANGEBYSCORE → ZREM gap and both think they own the same job. With Lua, the entire 5-step sequence is one atomic operation — Redis runs Lua single-threaded.

reapStuck uses a sibling Lua script to find expired-lease claimed jobs and move them back to due.

Storage layout

All keys prefixed by keyPrefix (default 'absolutejs:queue:'):

job: — HASH per job (the record)

due — ZSET keyed by runAt

Show 2 more

claimed — ZSET keyed by lockedAt + leaseMs (lease expiry)

idempotency: — STRING mapping idempotency key → job id

v0.0.1 surface

Method — Status

enqueue (with idempotency) — ✓

claimDue (atomic Lua) — ✓

Show 7 more

complete — ✓

fail (with retry / dead-letter) — ✓

reapStuck (atomic Lua) — ✓

get — ✓

countByStatus — partial — counts pending + claimed; v0.0.1 returns 0 for done/dead/canceled

cancel, list, listByKind, retry — deferred to 0.1.0

The deferred methods need a separate maintained index (byKind: SET, byStatus: SET) which would cost a write on every status transition. v0.0.1 ships without to keep the hot path lean; 0.1.0 adds opt-in lazy indexing.

Crash safety

Redis pub/sub-style queues are subject to your Redis durability config:

AOF on, fsync every second: at most 1s of work lost on Redis crash

AOF on, fsync always: no work lost, slower

Show 2 more

RDB only: minutes of work lost — NOT recommended for queues

For at-most-once semantics, Redis durability is sufficient. For at-least-once, the worker should mark a job complete/failed only AFTER the side effect succeeds — @absolutejs/queue's worker already does this.

Outcomes

What you can build

Build on the supported package contract

Use @absolutejs/queue-redis through its supported public entry points.

Hardening checklist

Production guidance

Make every external boundary explicitPin the deployed @absolutejs/queue-redis version, replace example or memory-backed dependencies with durable implementations, bound external calls, protect credentials, and emit enough evidence to retry or recover safely.

Follow in order

Troubleshooting path

1
Trace from the first failed boundary
Reproduce the smallest canonical @absolutejs/queue-redis example, confirm the supported entry point and version in the API explorer, then inspect the first boundary that did not produce its documented result.

#Usage with ioredis

Partial snippet

Working example for Usage with ioredis.

TS
import { Redis } from 'ioredis';
import {
  createJobRegistry,
  createQueueWorker,
  defineJobs,
  t,
  type JobMapFromDefinition
} from '@absolutejs/queue';
import { createRedisJobStore } from '@absolutejs/queue-redis';

const redis = new Redis(process.env.REDIS_URL!);

const jobs = defineJobs({
  'email.send': t.Object({ to: t.String(), subject: t.String() }),
});

const store = createRedisJobStore<JobMapFromDefinition<typeof jobs>>({
  client: redis,                    // ioredis structurally satisfies RedisCommandClient
  keyPrefix: 'myapp:queue:',         // optional
});

const registry = createJobRegistry(jobs).on('email.send', async (payload) => {
  await sendEmail(payload);
});

const worker = createQueueWorker({ store, registry });
worker.start();

#Usage with node-redis v4+

Partial snippet

Working example for Usage with node-redis v4+.

TS
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

// node-redis's typed wrappers have slightly different signatures
// (e.g. `client.hSet(key, fields)` is camelCase). Adapt:
const adapted: RedisCommandClient = {
  hset: (key, fields) => client.hSet(key, fields),
  hgetall: (key) => client.hGetAll(key),
  hdel: (key, ...fields) => client.hDel(key, fields),
  del: (...keys) => client.del(keys),
  zadd: (key, score, member) => client.zAdd(key, { score, value: member }),
  zrem: (key, ...members) => client.zRem(key, members),
  zrangebyscore: (key, min, max, offset, count) =>
    client.zRangeByScore(key, min, max,
      offset !== undefined && count !== undefined
        ? { LIMIT: { offset, count } }
        : undefined),
  zcard: (key) => client.zCard(key),
  sadd: (key, ...m) => client.sAdd(key, m),
  srem: (key, ...m) => client.sRem(key, m),
  set: (key, value, mode) => client.set(key, value, mode === 'NX' ? { NX: true } : undefined),
  get: (key) => client.get(key),
  scard: (key) => client.sCard(key),
  smembers: (key) => client.sMembers(key),
  eval: (script, keys, args) => client.eval(script, { keys, arguments: args }),
};

const store = createRedisJobStore({ client: adapted });

#Public entry points

Supported entry points declared by this package manifest.

Package entry point declared in package.json.

@absolutejs/queue-redis@absolutejs/queue-redis/manifest@absolutejs/queue-redis/manifest.json

#Package commands

Scripts declared by this package manifest.

bun run buildrm -rf dist && bun build src/index.ts src/manifest.ts --outdir dist --sourcemap --target=bun --external @absolutejs/queue && tsc --project tsconfig.build.json && absolute-manifest emit
bun run formatprettier --write "./**/*.{ts,json,md}"
bun run testbun test
bun run typechecktsc --noEmit

#API reference

Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.

3 symbols
RedisCommandClienttypePermalinkSource

Minimal Redis command surface. Both ioredis and node-redis v4+ structurally satisfy this (their typed wrappers expose the same names + signatures). Reads/writes pass through as strings.

TS
type RedisCommandClient = {
    hset: (key: string, fields: Record<string, string>) => Promise<unknown>;
    hgetall: (key: string) => Promise<Record<string, string> | null>;
    hdel: (key: string, ...fields: string[]) => Promise<unknown>;
    del: (...keys: string[]) => Promise<unknown>;
    zadd: (key: string, score: number, member: string) => Promise<unknown>;
    zrem: (key: string, ...members: string[]) => Promise<unknown>;
    zrangebyscore: (key: string, min: number | string, max: number | string, offset?: number, count?: number) => Promise<string[]>;
    zcard: (key: string) => Promise<number>;
    sadd: (key: string, ...members: string[]) => Promise<unknown>;
    srem: (key: string, ...members: string[]) => Promise<unknown>;
    set: (key: string, value: string, mode?: 'NX') => Promise<string | null>;
    get: (key: string) => Promise<string | null>;
    scard: (key: string) => Promise<number>;
    smembers: (key: string) => Promise<string[]>;
    /**
     * Execute a Lua script with the given KEYS and ARGV. Both ioredis
     * (`eval(script, numkeys, ...keysAndArgs)`) and node-redis
     * (`EVAL` via `sendCommand` or its scripting API) satisfy this
     * via adapter wr
Exported from @absolutejs/queue-redis