Build on the supported package contract
Use @absolutejs/queue-redis through its supported public entry points.
@absolutejs/queue-redisv0.0.3betaData & SyncRedis-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.
bun add @absolutejs/queue-redisRedis-backed JobStore for @absolutejs/queue. Sibling to @absolutejs/queue-postgres — same JobStore contract, different transport.
Docs: absolutejs.com/documentation/queue-overview#redis-adapter
Concern — queue-redis — queue-postgres
Already have Redis — Win — New dep to operate
Already have Postgres — Skip — fewer deps — Win
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.
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
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.
All keys prefixed by keyPrefix (default 'absolutejs:queue:'):
job: — HASH per job (the record)
due — ZSET keyed by runAt
claimed — ZSET keyed by lockedAt + leaseMs (lease expiry)
idempotency: — STRING mapping idempotency key → job id
Method — Status
enqueue (with idempotency) — ✓
claimDue (atomic Lua) — ✓
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.
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
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
Use @absolutejs/queue-redis through its supported public entry points.
Hardening checklist
Follow in order
Working example for Usage with ioredis.
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();Working example for Usage with node-redis v4+.
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 });Supported entry points declared by this package manifest.
Package entry point declared in package.json.
Scripts declared by this package manifest.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
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.
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@absolutejs/queue-redis