DEV Community

Cover image for I Built a Job Queue That's 32x Faster Than BullMQ (No Redis Required)
Egeo Minotti
Egeo Minotti

Posted on • Originally published at egeominotti.github.io

I Built a Job Queue That's 32x Faster Than BullMQ (No Redis Required)

I got tired of spinning up Redis just to run background jobs. So I built bunqueue - a job queue that uses SQLite instead.

The Problem

Every time I start a new project:

# Here we go again...
docker run -d redis
npm install bullmq ioredis
Enter fullscreen mode Exit fullscreen mode

For small to medium projects, Redis is overkill. You need:

  • Another container to manage
  • Another service to monitor
  • Another thing that can fail at 3 AM

The Solution

bun add bunqueue
Enter fullscreen mode Exit fullscreen mode

That's it. No Redis. No Docker. Just SQLite.

import { Queue, Worker } from 'bunqueue/client';

// Create a queue
const queue = new Queue('emails');

// Add a job
await queue.add('welcome', {
  to: 'user@example.com'
});

// Process jobs
new Worker('emails', async (job) => {
  await sendEmail(job.data);
  return { sent: true };
});
Enter fullscreen mode Exit fullscreen mode

Why SQLite?

Bun has native SQLite support (bun:sqlite) that's incredibly fast. With WAL mode enabled:

  • 500,000+ ops/sec for queue operations
  • 32x faster than BullMQ for common patterns
  • Zero network latency - it's just a file

Features You Actually Need

bunqueue isn't a toy. It has everything for production:

Feature
Retries with backoff
Job priorities
Delayed jobs
Cron scheduling
Dead Letter Queue
Stall detection
Progress tracking
S3 backups
Sandboxed workers

Real Example: Email Queue

import { Queue, Worker } from 'bunqueue/client';

interface EmailJob {
  to: string;
  subject: string;
  template: string;
}

const emailQueue = new Queue<EmailJob>('emails');

// Add with retry options
await emailQueue.add('welcome', {
  to: 'user@example.com',
  subject: 'Welcome!',
  template: 'welcome'
}, {
  attempts: 3,        // Retry 3 times
  backoff: 5000,      // Wait 5s between retries
  priority: 10,       // Higher = processed first
});

// Worker with concurrency
const worker = new Worker<EmailJob>('emails', async (job) => {
  await job.updateProgress(10, 'Loading template');

  const html = await renderTemplate(job.data.template);

  await job.updateProgress(50, 'Sending');

  await sendEmail({
    to: job.data.to,
    subject: job.data.subject,
    html
  });

  await job.updateProgress(100, 'Done');
  return { sent: true };
}, {
  concurrency: 5  // Process 5 emails in parallel
});

worker.on('completed', (job, result) => {
  console.log(`✅ Email sent to ${job.data.to}`);
});

worker.on('failed', (job, error) => {
  console.error(`❌ Failed: ${job.data.to}`, error.message);
});
Enter fullscreen mode Exit fullscreen mode

CPU-Intensive? Use Sandboxed Workers

For heavy tasks that might crash or leak memory:

import { SandboxedWorker } from 'bunqueue/client';

const worker = new SandboxedWorker('video-processing', {
  processor: './video-processor.ts',
  concurrency: 4,
  timeout: 300000,    // 5 min timeout
  maxMemory: 512,     // MB per worker
  maxRestarts: 10,    // Auto-restart on crash
});

worker.start();
Enter fullscreen mode Exit fullscreen mode

Each job runs in an isolated Bun subprocess. If it crashes, only that worker restarts.

BullMQ Migration

Already using BullMQ? The API is compatible:

- import { Queue, Worker } from 'bullmq';
+ import { Queue, Worker } from 'bunqueue/client';

// Your code stays the same
const queue = new Queue('my-queue');
Enter fullscreen mode Exit fullscreen mode

When to Use bunqueue

Use bunqueue when:

  • Single server or small cluster
  • You want simplicity
  • You don't want to manage Redis
  • You need fast local queues

Stick with BullMQ/Redis when:

  • Multi-region distributed systems
  • You already have Redis infrastructure
  • You need pub/sub beyond job queues

Get Started

bun add bunqueue
Enter fullscreen mode Exit fullscreen mode
import { Queue, Worker } from 'bunqueue/client';

const queue = new Queue('tasks');

await queue.add('hello', { message: 'world' });

new Worker('tasks', async (job) => {
  console.log(job.data.message);
});
Enter fullscreen mode Exit fullscreen mode

Links:


Built with Bun. Because sometimes the simple solution is the right one.

Top comments (0)