tl-pg-boss
tl-pg-boss
PostgreSQL-backed job queue for Node.js with exactly-once delivery, cron scheduling, and transactional safety.
When to Use
- "Add background jobs to my app"
- "I need a job queue but already use Postgres"
- "Set up cron jobs / scheduled tasks"
- "Process async work with retries"
- User mentions: job queue, background processing, task scheduling, worker
When to Consider Alternatives
For managed background jobs with dashboard UI, consider Trigger.dev instead. pg-boss is best when:
- You want self-hosted, PostgreSQL-native job queues
- You already have Postgres and don't want external dependencies
- You need transactional job enqueuing (same transaction as your data writes)
- Cost matters (pg-boss is free, managed services charge per job)
Trigger.dev is better when:
- You need a polished dashboard out of the box
- You want managed infrastructure with auto-scaling
- Your team prefers a hosted SaaS experience
- You need complex workflows with visual orchestration
Outcomes
- Patch: pg-boss installed and configured with typed job handlers
- Artifact: Job definitions with proper queue setup
- Decision: Queue structure, job patterns, monitoring approach
Requirements
| Requirement | Version |
|---|---|
| Node.js | 22.12+ |
| PostgreSQL | 13+ |
| Privilege | CREATE on database |
Installation
pnpm add pg-boss
Auto-creates pgboss schema on first start(). No manual migrations needed.
Core Concepts
SKIP LOCKED
PostgreSQL's SKIP LOCKED provides exactly-once delivery without distributed transactions.
Always Call start()
Critical: Every process must call start(), even producers.
const boss = new PgBoss(connectionString);
await boss.start(); // Required in EVERY process
Even with multiple processes calling start(), only one runs supervision.
One Queue Per Job Type
await boss.createQueue("send-email");
await boss.createQueue("process-image");
Basic Setup
import { PgBoss } from "pg-boss";
const boss = new PgBoss({
connectionString: process.env.DATABASE_URL,
schema: "pgboss",
});
boss.on("error", console.error);
await boss.start();
await boss.createQueue("my-queue");
Job Patterns
Send a Job
const jobId = await boss.send("my-queue", { userId: "123" });
Send with Options
await boss.send("my-queue", payload, {
retryLimit: 3,
retryDelay: 60,
expireInMinutes: 30,
priority: 1,
});
Full options: See
references/send-options.md
Delayed Job
await boss.send("my-queue", payload, {
startAfter: new Date(Date.now() + 60000),
});
Cron Scheduling
await boss.schedule("daily-report", "0 9 * * *", { type: "daily" });
Unschedule (Remove Cron)
await boss.unschedule("daily-report");
Schedule management: See
references/schedule-management.md
Worker Patterns
Basic Worker
await boss.work("my-queue", async ([job]) => {
console.log(`Processing ${job.id}`);
// Auto-completes on return, throw to fail
});
Note: Callback receives an array even with batchSize: 1.
Batch Processing
await boss.work("bulk-import", { batchSize: 10 }, async (jobs) => {
for (const job of jobs) {
await processItem(job.data);
}
});
Typed Jobs
interface EmailJob {
to: string;
subject: string;
}
await boss.send<EmailJob>("send-email", { to: "user@example.com", subject: "Hi" });
await boss.work<EmailJob>("send-email", async ([job]) => {
await sendEmail(job.data.to, job.data.subject);
});
TypeScript patterns: See
references/typescript-patterns.md
Queue Configuration
Dead Letter Queue
await boss.createQueue("my-queue", { deadLetter: "my-queue-dlq" });
await boss.createQueue("my-queue-dlq");
Retention
await boss.createQueue("my-queue", { retentionMinutes: 60 * 24 });
Fastify Integration
import Fastify from "fastify";
import { PgBoss } from "pg-boss";
const fastify = Fastify();
const boss = new PgBoss(process.env.DATABASE_URL);
fastify.decorate("boss", boss);
fastify.addHook("onReady", async () => {
boss.on("error", fastify.log.error.bind(fastify.log));
await boss.start();
await boss.createQueue("my-queue");
await boss.work("my-queue", handler);
});
fastify.addHook("onClose", async () => {
await boss.stop({ graceful: true });
});
Monitoring
Dashboard
pnpm add @pg-boss/dashboard
DATABASE_URL="postgres://..." npx pg-boss-dashboard
Quick SQL
-- Pending by queue
SELECT name, COUNT(*) FROM pgboss.job WHERE state = 'created' GROUP BY name;
-- Failed jobs
SELECT * FROM pgboss.job WHERE state = 'failed' ORDER BY completedon DESC LIMIT 20;
Full monitoring: See
references/monitoring.md
Sharp Edges
| Gotcha | Solution |
|---|---|
Must call start() everywhere |
Even producers need it |
| Jobs array in worker | Use ([job]) not (job) |
| No LISTEN/NOTIFY | Polling only, set pollingIntervalSeconds |
| Schema needs CREATE privilege | Or use CLI: npx pg-boss migrate --dry-run |
| Once completed, can't fail | Don't mix work() with manual fail() |
No pauseQueue() in v10 |
Use unschedule() + direct SQL |
| Schedules re-register on restart | Code calls schedule() on init; unschedule is temporary |
Best Practices
- Set expiration to prevent zombies:
expireInMinutes: 30 - Archive aggressively with retention policies
- Idempotent handlers using upserts
- Graceful shutdown:
boss.stop({ graceful: true })
Observability
See Observability for Prometheus gauge/counter setup with
prom-clientand Prometheus alerting rules for queue backlog and failure rate.
Testing Patterns
See Testing Patterns for unit testing job handlers, integration testing with a real PostgreSQL database, and mocking pg-boss in vitest.
Kubernetes Deployment
See Kubernetes Deployment for a worker Deployment manifest with resource limits, liveness probe, preStop hook, and the SIGTERM graceful-shutdown handler.
Verification
-
boss.start()completes without error -
boss.createQueue()succeeds -
boss.send()returns a job ID - Worker processes the job
- Job state changes to
completed
References
Quilted Skills
- triggerdotdev/skills/trigger-tasks — Workflow patterns
- omer-metin/skills-for-antigravity/pg-boss — SKIP LOCKED principles
First-Party Documentation
- pg-boss GitHub — Official repository
- pg-boss API Docs — API reference
- pg-boss Dashboard — Web UI
PostgreSQL Resources
- PostgreSQL SKIP LOCKED — Locking semantics
- PostgreSQL Advisory Locks — Application-level locks
- Connection Pooling with PgBouncer — Connection management
Alternative Solutions
- Graphile Worker — PostgreSQL queue alternative
- Trigger.dev — Managed background jobs
- BullMQ — Redis-based alternative
Skill References
references/send-options.md— Full send optionsreferences/typescript-patterns.md— BaseJob, JobManagerreferences/monitoring.md— Dashboard + SQL queriesreferences/advanced-patterns.md— Singleton, throttling, pub/subreferences/schedule-management.md— Cron schedules, unschedule, pause patternsreferences/wordpress-migration.md— WP-Cron & Action Scheduler mapping
Attribution
| Source | Author | Contribution |
|---|---|---|
| pg-boss | Tim Gilbert (@timgit) | Core library, official docs |
| TypeScript Deep Dive | Shayan (@ImSh4yy) | BaseJob class, JobManager pattern |
| pg-boss-admin-dashboard | Lyubomir Petrov (@lpetrov) | Alternative dashboard with JMESPath |
More from toddlevy/tl-agent-skills
tl-openmeter-api
Works with the OpenMeter REST API for usage metering, billing, and entitlements. Covers CloudEvents ingestion, meters, features, plans, customers, subscriptions, entitlements, notifications, billing profiles, invoices, apps, addons, grants, and the Stripe marketplace. Use when integrating OpenMeter, debugging metering, building catalog sync scripts, or when the user mentions OpenMeter API.
15tl-first-principles
Foundational software design principles traced to their intellectual origins. Covers information hiding, separation of concerns, abstraction, SSOT/DRY, conceptual integrity, and composition. Use when making architectural decisions, evaluating trade-offs, or understanding *why* best practices exist.
15tl-knip
Find and remove unused files, dependencies, and exports in TypeScript/JavaScript projects using Knip. Covers configuration-first workflow, plugin system, barrel file handling, CI integration, monorepo support, and agent-specific cleanup guidance.
14tl-docs-create
Create documentation from scratch for codebases. Covers SSOT-driven generation, writing standards, and templates for README/AGENTS.md/CHANGELOG. Use when creating new docs or documenting an undocumented codebase.
14tl-devlog
Maintain a structured development changelog (DEVLOG.md) capturing architectural decisions, milestones, incidents, and insights. Use when the user says "log this", "devlog", "archive this", or at natural pause points after significant decisions. Trigger on changelog, decision log, work log, or progress tracking.
14tl-docs-audit
Audit existing documentation for gaps, staleness, and sync issues. Generates sync reports with actionable findings. Use when reviewing doc coverage, finding outdated docs, or syncing docs with code.
14