Background jobs,
batteries included.

OpenQueue is a TypeScript job framework built on BullMQ and Redis. Typed tasks, retries, cron, flows, and a dashboard you didn't have to build — one config file, one CLI, zero glue code.

Get started →

MIT · built on BullMQ · runs on Bun & Node

openqueue · my-app · 4 queues

exports

47 active · 1,204 waiting · last 24h

completed

184,392

+12.4%

failed

287

-3.1%

active

47

+8/min

waiting

1,204

stable

Throughput · jobs/min

completed failed

p50 wait12ms
p95 duration847ms
BullMQRedisZodBunNext.jsDrizzleOpenTelemetryDocker

Tasks

If you can write a function, you can ship a job.

A task is an id, a schema, and a function. OpenQueue wires up the queue, the worker, validation, retries, backoff, and concurrency — then hands you a typed .trigger() to call from anywhere in your app.

  • Payloads validated by Zod before they ever hit Redis.
  • Logs and progress stream straight into the dashboard.
  • Defaults that make sense: 3 attempts, exponential backoff.
worker/export-csv.tstypescript
import { task } from '@openqueue/sdk';
import { z } from 'zod';

export const exportCsv = task({
  id: 'export-csv',
  schema: z.object({
    reportId: z.string(),
    format: z.enum(['csv', 'xlsx']),
  }),
  attempts: 5,
  backoff: { type: 'exponential', delay: 1000 },
  run: async (payload, ctx) => {
    ctx.logger.info('building report', { id: payload.reportId });
    const file = await build(payload);
    await ctx.progress({ step: 'uploading' });
    return { url: file.url };
  },
});

// anywhere in your app — typed, validated, durable
await exportCsv.trigger({ reportId: 'rep_812', format: 'csv' });

Runs

Inspect every run. Replay any failure.

Full payloads, attempt history, logs, and progress for every job. Filter by status, search by ID, retry from the UI — and keep history in Postgres long after Redis forgets.

exports · last 50 runs

Runs 184,679

All 184kActive 47Completed 184kFailed 287
Job IDNameDurationAttemptsAge
r_3aa18export-csv active1/3now
r_3aa17send-invoice active1/32s
r_3aa16export-csv completed284ms1/38s
r_3aa15sync-contacts completed1.2s1/314s
r_3aa14export-csv failed1.8s3/322s
r_3aa13ocr-document completed612ms1/331s
r_3aa12send-invoice completed4.7s2/344s
r_3aa11send-receipt delayed0/3in 2m
r_3aa10export-csv completed198ms1/31m
r_3aa09sync-contacts waiting0/31m

Errors

Triage failures, not log files.

Failures are grouped by error class, ranked by frequency, and trended over time — so a regression shows up as a spike the moment it ships, not as a support ticket three days later.

my-app · errors · last 24h

Top errors · last 24h

287 events

  • ECONNRESET184
  • TimeoutError63
  • ValidationError28
  • S3AccessDenied12

ECONNRESET · exports · spiking +240% vs yesterday · first seen 4h ago

Flows

Pipelines as parent/child tasks.

enqueueFlow turns task.child trees into real DAGs with per-node status and duration. Drill into any node and see exactly where the time went.

order-fulfillment · flow_4f12

order-fulfillment

flow_4f12 · 7 jobs · 2.4s total

✓ completed

validate-cart

completed 42ms

charge-card

completed 612ms

reserve-stock

completed 118ms

email-receipt

completed 287ms

generate-pdf

completed 894ms

notify-warehouse

completed 71ms

finalize

completed 33ms

Schedules

Cron you can change without redeploying.

Declare cron on a task for static schedules, or create and pause them at runtime with task.schedules.create(). See what runs next, what ran last, and how long it took.

my-app · 6 schedules

Schedules

5 active · next: in 2m

NameCronNextLast run
weekly-digest0 9 * * MONin 2d 4hok · 1.2s
cleanup-stale*/15 * * * *in 4mok · 87ms
reindex-search0 */6 * * *in 1h 12mok · 4.7s
trial-reminders0 10 * * *in 18hok · 612ms
❚❚rotate-creds0 0 1 * *in 6dok · 28ms
warm-cache*/5 * * * *in 2mok · 142ms

What's inside

Everything a job framework should do. Nothing it shouldn't.

No proprietary runtime, no per-job pricing, no second cloud. Your Redis, your Postgres, your code — OpenQueue just makes them feel like a product.

Typed payloads

Give a task a Zod schema and payloads are validated at trigger time and fully typed inside run — no any, no guessing.

Retries with taste

Exponential backoff by default. Throw NonRetryableError to stop, RetryableError to keep going — control flow, not string matching.

Cron + live schedules

Declare cron on the task, or create, list, and pause schedules at runtime through the schedules API. No redeploy to change a cadence.

Flows & DAGs

Compose parent/child trees with enqueueFlow and task.child. Per-node status and duration, rendered as a real graph.

Workbench built in

Overview, runs, flows, schedules, errors, metrics, and a test console — served by your worker, or mounted in your own app.

Postgres run history

The Drizzle adapter persists runs, logs, and alerts past Redis retention. Your audit trail survives a FLUSHALL.

OpenTelemetry native

Trace context propagates from enqueue to process automatically. Plug in any exporter and see jobs inside your existing traces.

Hot-reload dev

openqueue dev watches your task files and restarts the worker in milliseconds. The dashboard reconnects on its own.

CLI

Four commands. That's the whole workflow.

init scaffolds a config and your first task. dev runs the worker with hot reload. build compiles a self-contained server artifact, and start runs it in production. Failed runs retry with backoff and land in the dashboard — not in your logs at 3am.

zsh — my-app
$ bunx @openqueue/cli init
created worker.config.ts
created worker/example.ts
OpenQueue initialized

$ openqueue dev
[openqueue] 3 tasks · 2 queues · 1 schedule
[openqueue] workbench on http://localhost:8090/workbench
[openqueue] watching ./worker for changes

 export-csv r_3aa16 · 284ms
 sync-contacts r_3aa15 · 1.2s
 export-csv r_3aa14 · attempt 2/3 · retrying in 2s

Run it

One worker. Three ways to run it.

Develop

Next to your app

openqueue dev discovers tasks from your worker/ directory, hot-reloads on change, and serves Workbench locally. One process, no Docker, no YAML.

$ openqueue dev

Embed

Inside your server

Mount the dashboard in the app you already run. h3 and Next.js App Router adapters — or a plain fetch handler anywhere else — one catch-all route and Workbench shares your auth.

$ import { workbench } from '…/next'

Ship

In a container

openqueue build compiles a self-contained Nitro server artifact; openqueue start runs it. Or point the prebuilt Docker worker at your Redis and go.

$ openqueue build && openqueue start

Proven in production

It already runs a business.

OpenQueue handles every background job at Expensicat — invoicing, inbox OCR, bank sync, transaction matching, notifications. Same tasks, same retries, same Workbench you see above, in production around the clock.

ExpensicatExpensicatexpensicat.com →

Stop babysitting Redis.

You already run the infrastructure. OpenQueue turns it into a job platform — in about the time it takes your CI to go green.

Read the quickstart →