Getting Started
PortQueue is a background-job queue that runs on the database you already have — PostgreSQL, MySQL, Oracle, PGLite or SQLite. No broker and no second datastore to run: your jobs live in the same database as the rest of your data, so they commit with your own writes and you can query them with SQL.
Install
Install @portqueue/core plus the client for your database — the DB clients are
optional peer dependencies, never auto-installed, so you only pull in what you use.
Requires Node.js 20.11 or newer.
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
npm install @portqueue/core pg
npm install @portqueue/core mysql2
npm install @portqueue/core oracledb
npm install @portqueue/core @electric-sql/pglite
npm install @portqueue/core better-sqlite3
Prefer another package manager? The install is identical apart from the command — the DB client tracks the engine you picked above:
- npm
- pnpm
- yarn
- bun
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
npm install @portqueue/core pg
npm install @portqueue/core mysql2
npm install @portqueue/core oracledb
npm install @portqueue/core @electric-sql/pglite
npm install @portqueue/core better-sqlite3
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
pnpm add @portqueue/core pg
pnpm add @portqueue/core mysql2
pnpm add @portqueue/core oracledb
pnpm add @portqueue/core @electric-sql/pglite
pnpm add @portqueue/core better-sqlite3
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
yarn add @portqueue/core pg
yarn add @portqueue/core mysql2
yarn add @portqueue/core oracledb
yarn add @portqueue/core @electric-sql/pglite
yarn add @portqueue/core better-sqlite3
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
bun add @portqueue/core pg
bun add @portqueue/core mysql2
bun add @portqueue/core oracledb
bun add @portqueue/core @electric-sql/pglite
bun add @portqueue/core better-sqlite3
A minimal queue
Construction wires dependencies only — no connection, no I/O. init() connects, migrates, and
starts services; it is idempotent, so awaiting it once is enough.
- PostgreSQL
- MySQL
- Oracle
- PGLite
- SQLite
import { PortQueue } from '@portqueue/core';
import { PostgresqlDriver } from '@portqueue/core/drivers/postgresql';
const pq = new PortQueue({
driver: new PostgresqlDriver({ connectionString: process.env.DATABASE_URL }),
});
await pq.init();
import { PortQueue } from '@portqueue/core';
import { MysqlDriver } from '@portqueue/core/drivers/mysql';
const pq = new PortQueue({
driver: new MysqlDriver({ connectionString: process.env.MYSQL_URL }),
});
await pq.init();
import { PortQueue } from '@portqueue/core';
import { OracleDriver } from '@portqueue/core/drivers/oracle';
const pq = new PortQueue({
driver: new OracleDriver({
connectString: process.env.ORACLE_DSN,
user: 'app',
password: process.env.ORACLE_PW,
}),
});
await pq.init();
import { PortQueue } from '@portqueue/core';
import { PgliteDriver } from '@portqueue/core/drivers/pglite';
const pq = new PortQueue({ driver: new PgliteDriver({ dataDir: './pgdata' }) });
await pq.init();
import { PortQueue } from '@portqueue/core';
import { SqliteDriver } from '@portqueue/core/drivers/sqlite';
const pq = new PortQueue({ driver: new SqliteDriver({ path: './queue.db' }) });
await pq.init();
With pq running, declare the queue — its name, how hard it should pull, and the handler that
processes its jobs — then add a job to it:
pq.createQueue<{ to: string }>({
name: 'emails',
limiter: { concurrency: 10 }, // up to 10 handlers at once in this process
handler: async (job) => {
await sendEmail(job.data); // job.data is typed to the queue's payload
},
});
await pq.addJob({ queue: 'emails', data: { to: 'a@b.com' } });
That is the whole loop: processing starts on its own, so there is no worker to wire up and nothing to
call .start() on. Returning from the handler completes the job; throwing retries or fails it
according to the job's retry policy.
Swapping databases is the driver import above and nothing else.
Read on in Producing jobs for scheduling, priorities, retries and dedup, and Consuming jobs for concurrency, batching and cancellation.
Your mindset when using PortQueue
Everything hangs off pq. Adding, reading, pausing, retrying, removing — every operation is a
method on the client that names its queue: pq.addJob({ queue, data }),
pq.getJobs({ queue }), pq.pauseQueue({ queue }). The value createQueue returns has no methods,
so there is nothing to thread through your code.
Declaring a queue means processing it. createQueue always takes a handler, and one queue takes
one handler per process. To only enqueue, don't declare anything: pq.addJob({ queue, data }) works
from the queue's name alone.
One process or many — both are first class. Running everything in a single process (your app, its
handlers, the scheduler, the dashboard) is a perfectly good production setup, and the one to start
with. If you'd rather split the roles you can: a process that declares nothing can still addJob, so
an API tier can produce while a worker tier declares the same queue with a handler — same code,
different processes. See Multi-server.
Name your queues with an enum. new PortQueue<QueueName>({ … }) threads a string enum (or
literal union) of your queue names through every queue argument, so a typo is a compile error rather
than a job nobody consumes.
Scale by running more copies. Start another instance and it shares the work — nothing to
configure, and no job is processed twice. Give each process an instanceKey and you can pause, drain
and target them individually.
A job exists only if your write commits. Pass your own transaction as tx and the job is
inserted inside it. See Transactional enqueue.
The operational tooling is in the box. A mountable React dashboard for queues, jobs, schedules and instances; a CLI for the same from a terminal; typed events and hooks for metrics and tracing; and retention so the table never fills up on its own.
Want to see it running first? Every example app is self-contained and needs no database server.