Skip to content
Get started
Get started
Why Replace Redis with SQLite for Bun Job Queues
View Markdown
blog · design

A job queue without Redis.

BullMQ, Bee-Queue, and friends all assume a running Redis server. bunqueue can keep a durable queue in-process with SQLite instead: simpler deployments and zero external services, with a separate PostgreSQL multi-broker path when you scale. Unconfigured queues use memory storage.

Redis is excellent software, but it introduces real operational complexity for job queues:

  • Another service to manage - deploy, monitor, backup, scale
  • Network latency - every job operation crosses the network
  • Memory limits - Redis stores everything in RAM
  • Connection management - pool sizing, reconnection, timeouts

For many applications, especially single-server deployments and small teams, this overhead isn’t justified.

SQLite runs in-process. There’s no network hop, no connection pool, no separate service to manage.

Zero Dependencies

No Redis, no external services. Just your app and a file on disk, with msgpackr as the only runtime dependency.

In-Process Speed

Direct memory access instead of TCP round-trips to Redis.

Persistence When Selected

Set a SQLite data path for WAL-backed recovery with concurrent reads; without one, the queue is intentionally in memory.

Simple Backups

Copy one file, or use built-in S3 backup.

Bun’s native SQLite driver (bun:sqlite) is what makes this architecture viable. It’s not a Node.js addon compiled from C - it’s built directly into the runtime with zero-copy optimizations.

import { Database } from 'bun:sqlite';
const db = new Database('./queue.db', { create: true });
// WAL mode for concurrent reads + writes
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA synchronous = NORMAL');

This gives us prepared statements, transactions, and WAL mode with performance that rivals in-memory datastores for our workload patterns.

Raw SQLite writes are fast, but bunqueue goes further with a write buffer that batches disk operations:

// Jobs are added to an in-memory priority queue immediately
// The write buffer flushes to SQLite every 10ms
const queue = new Queue('emails', {
embedded: true,
dataPath: './data/bunqueue.db',
});
// This returns instantly - job is in memory
await queue.add('send', { to: 'user@example.com' });
// For critical jobs, bypass the buffer:
await queue.add('payment', data, { durable: true });

The durable: true option bypasses bunqueue’s write buffer and commits to SQLite before add() returns, trading throughput for a smaller process-crash window. Host, filesystem, and physical-media durability still depend on the deployment.

bunqueue’s SQLite mode is designed for single-broker deployments. Choose it when:

  • You run your app on a single server or VPS
  • You want zero operational overhead for background jobs
  • You need reliable persistence without managing Redis
  • You’re building on Bun and want native performance

Choose bunqueue’s PostgreSQL 15–18 server backend when several active brokers must share one authoritative queue; PostgreSQL 18.6 is recommended. The Queue, Worker, Flow, cron, retry, result, and DLQ contracts stay the same. BullMQ remains a solid choice when Redis or Redis Cluster is already your required operational standard. See Storage backends for the topology boundary.

Two deep dives into bunqueue’s architecture pick up where this post leaves off: the sharding deep dive covers how jobs are distributed across CPU cores, and the auto-batching post explains how TCP mode gets close to embedded-mode throughput for concurrent producers.