Skip to content

lolmatina/queue-lite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

queue-lite

A TypeScript-first, durable job queue for Node/Bun — without Redis. It uses the database you already have: SQLite by default (zero native deps via bun:sqlite), Postgres as a drop-in for multi-server, and an in-memory backend for tests.

import { z } from "zod";
import { createQueue, defineJob, SqliteAdapter } from "queue-lite";

const sendEmail = defineJob({
  name: "sendEmail",
  schema: z.object({ to: z.string().email(), subject: z.string() }),
  handler: async ({ payload }) => {
    await mailer.send(payload.to, payload.subject);
  },
});

const queue = createQueue({
  storage: new SqliteAdapter("queue.db"),
  jobs: [sendEmail],
  worker: { concurrency: 5 },
});

await queue.start();

await queue.enqueue("sendEmail", { to: "alice@example.com", subject: "Welcome!" });
// compile error - wrong payload shape:
// await queue.enqueue("sendEmail", { to: "alice@example.com" });
// compile error - unknown job name:
// await queue.enqueue("sendEmial", { to: "...", subject: "..." });

Why

Every mature Node job queue needs Redis: friction in dev, cost in deploy, a coordination point at scale. queue-lite removes that — jobs live in your existing database, survive restarts, retry on failure, and run scheduled work without a cron daemon. When you truly outgrow it, switch to Postgres by changing one line.

Features

  • Type-safe enqueue — payloads are inferred from each job's Zod schema; wrong name or shape is a compile error, and everything is validated again at runtime.
  • Durable — jobs are written to storage before returning; nothing is lost on crash or deploy.
  • Retries with backoff — fixed or exponential, with jitter, configurable per job.
  • Delays and priorities — run a job later (delayMs/runAt) or ahead of others.
  • Cron schedules — recurring jobs that fire exactly once across instances.
  • Lifecycle eventsenqueued, started, completed, failed, retried, stalled for logging/metrics/alerting.
  • Stalled-job recovery — leases expire and orphaned jobs are reclaimed.
  • CLI — inspect and manage the queue from your terminal.
  • Pluggable storage — SQLite, Postgres, Memory; one interface, easy to extend.

Install

bun add queue-lite zod
# Postgres backend (optional):
bun add postgres

Storage backends

import { SqliteAdapter, PostgresAdapter, MemoryAdapter } from "queue-lite";

new SqliteAdapter("queue.db");            // file-backed (default)
new SqliteAdapter(":memory:");            // ephemeral
new PostgresAdapter(process.env.PG_URL!); // multi-server
new MemoryAdapter();                      // tests

Switching backends changes only this one line — your job code is unchanged.

Separate worker processes

Because jobs live in the storage backend, a producer and a worker can run as separate processes (or machines) as long as they share storage:

Backend Shared across processes? Use for
MemoryAdapter No — state is in-process only Tests, single-process apps
SqliteAdapter Yes, same machine (shared file) Single-host producer + worker(s)
PostgresAdapter Yes, across machines Multi-server / horizontal scale

Important

MemoryAdapter does not work with a separate worker process. Each process gets its own empty in-memory store, so a worker started in a different process never sees jobs enqueued elsewhere. (Two Queue instances in the same process can share one MemoryAdapter instance.) To run a worker as a separate process, use SqliteAdapter (same machine) or PostgresAdapter (any machine).

Runtime support

The library runs on both Bun and Node. The SQLite adapter picks a driver automatically:

Runtime SQLite driver Notes
Bun bun:sqlite Built-in, zero deps
Node >= 22.5 node:sqlite Built-in (older versions need --experimental-sqlite)
Any Node better-sqlite3 Fallback, used if installed

So new SqliteAdapter("queue.db") works the same on Bun and Node. On older Node without node:sqlite, bun add better-sqlite3 (an optional peer) and it is used automatically. The Postgres and in-memory backends have no runtime constraints.

Scheduling, delays, priorities

// Every weekday at 9am
await queue.schedule("dailyReport", "0 9 * * 1-5", { team: "growth" });

// Run 3 days from now
await queue.enqueue("followUp", { userId }, { delayMs: 3 * 24 * 60 * 60 * 1000 });

// Jump the line
await queue.enqueue("chargeCard", { invoiceId }, { priority: 10 });

// Dedupe
await queue.enqueue("syncUser", { userId }, { idempotencyKey: `sync:${userId}` });

Inspecting state

await queue.stats();           // { pending, active, failed, ... }
await queue.list({ status: "failed", limit: 20 });
await queue.getJob(id);
await queue.history(id);       // every attempt + error
await queue.retry(id);         // requeue a failed job
await queue.cancel(id);        // cancel a pending/delayed job

CLI

queue-lite stats --db queue.db
queue-lite list --status failed
queue-lite show <job-id>
queue-lite retry <job-id>
queue-lite cancel <job-id>
queue-lite stats --pg "$PG_URL" --json

Use --db <path> / QUEUE_LITE_DB for SQLite or --pg <conn> / QUEUE_LITE_PG for Postgres, and --json for machine-readable output.

Testing your jobs

Use the in-memory adapter and a FakeClock to drive the worker deterministically:

import { createQueue, MemoryAdapter, FakeClock } from "queue-lite";

const clock = new FakeClock(0);
const queue = createQueue({ storage: new MemoryAdapter(), jobs: [myJob], clock });

await queue.enqueue("myJob", { /* ... */ });
await queue.runWorkerOnce();           // process all ready jobs, no real timers
expect((await queue.stats()).completed).toBe(1);

Roadmap

The StorageAdapter interface and the CLI's read layer are deliberate seams, so these land without breaking the public API:

  • More databases — MySQL/MariaDB, LibSQL/Turso, MongoDB (implement the adapter and pass the conformance suite).
  • Richer CLIwork (run a worker), purge, schedules, --watch live tail.
  • Web dashboard — an optional queue-lite/web served via Bun.serve, reusing the same adapter calls and streaming queue.events over SSE.

Not for

Extreme-throughput pipelines (tens of thousands of jobs/sec) — reach for BullMQ and Redis. queue-lite is for everything else, with a clear upgrade path.

License

MIT

About

TypeScript-first, durable job queue for Node/Bun. No Redis — uses the database you already have.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors