Dashboard
A web-based dashboard is available in the @pg-boss/dashboard package for monitoring and managing jobs, queues and schedules.
Features
- Overview: Aggregate statistics, problem queues, and recent warnings at a glance
- Queue Management: View all queues with cached statistics and create new queues
- Job List: View jobs with state and queue filtering
- Job Details: View full job payloads, output data, and metadata
- Job Actions: Create, cancel, retry, resume, or delete jobs directly from the UI
- Warning History: When
persistWarningsis enabled, browse through previously emitted warning events - Multi-Schema Support: Monitor multiple pg-boss instances from a single dashboard
- Mobile Responsive: Full functionality on mobile devices with collapsible sidebar
- Shareable URLs: Database selection and filters are preserved in URLs for easy sharing
Pages
Overview
Landing page with aggregate counts across all queues (queued, deferred, ready, active, failed and total jobs), the top queues by backlog with a ready-count trend sparkline and status badge, and the most recent warnings. The Send Job button opens a form to enqueue a job into any queue.

Jobs
Recently created jobs across all queues. Filter by job ID, queue, state (pending, created, retry, active, completed, cancelled or failed) and minimum retry count, or open the advanced filters to match on keys inside the job's data or output JSON. Jobs list newest created first, or newest finished first with the completed, cancelled or failed filter. Manage view lets you add custom columns sourced from job fields (for example data.tenantId), and Copy link produces a shareable URL with the current filters and columns. Clicking a job opens its detail page.

Job detail
One job: its state, what it's waiting for or why it stopped, its data and output, and cancel / retry / resume / delete actions. A failed job leads with its error and the attempt it failed on. A running job shows how long it has been running, its heartbeat, and a countdown to its time limit, and the page picks up a new state without a reload. A completed job opens on its output.
The timeline shows the times the job's row keeps. A retry overwrites the start time and output, so earlier attempts appear only as a count. The last entry is when retention deletes the job: deleteAfterSeconds after it finished, or at keepUntil if it never ran.
A job copied into a dead letter queue shows why it's there, from the original job's output, and a lineage chain from the first job to this one. Only the first job (the root) and the job this one was copied from (the source) are recorded, so any hops between them show as a gap. Select either to see its timeline, taken from its own row while it still exists, or from what was copied onto this job once it has been deleted. The root is looked up in the queue a redrive returns jobs to, so a root redriven into a different queue is reported as not found.
Queues
Every queue in the schema with its policy and cached counts (queued, deferred, ready, active, failed, total), a trend sparkline, storage mode (shared or partitioned) and a status badge (idle, processing or backlogged). Columns are sortable and the list can be searched by name. Create Queue creates a new queue with its policy and retry / expiration / retention options. Clicking a queue opens its detail page with configuration, a 24-hour ready-count history and the jobs in that queue.

Schedules
Schedules registered with boss.schedule(), showing the target queue, optional key, expression, a human-readable frequency, the next occurrence and timezone. Both schedule kinds are read through pg-boss itself, so a schedule stored as an RRULE is badged as one and gets the same next occurrence the scheduling pass will use. Schedule Job creates a new schedule from either a cron expression or a rule, with a missed occurrence policy, and each schedule's detail page shows its data, options, the last job it created and lets you unschedule it.

Migrations
Status of background async migrations (BAM), the schema changes such as concurrent index builds that pg-boss runs outside the install transaction. Shows pending, in-progress, completed and failed counts, and for each command its version, target table, timestamps and the SQL that was (or will be) executed, including any error message.

Warnings
Event log of warnings emitted by pg-boss when persistWarnings is enabled, such as slow queries, queue backlogs or clock skew. Each entry shows its type, message, key details (for example the queue and its size, or the query duration) and time, and the list can be filtered by warning type.

Requirements
- Node.js 22.12+
- PostgreSQL database with pg-boss schema
- A pg-boss schema version that matches the dashboard release (see below)
Matching pg-boss versions
Each dashboard release is built with a specific pg-boss and only works against a database on that pg-boss's schema version. Use the dashboard release that matches the pg-boss version your application runs:
| Dashboard | pg-boss | Schema version |
|---|---|---|
| 1.12.x | 12.38.x | 46 |
| 1.11.x | 12.37.x | 45 |
| 1.10.x | 12.35.x | 43 |
| 1.9.x | 12.33.x – 12.34.x | 42 |
| 1.8.x | 12.31.x – 12.32.x | 41 |
| 1.7.x | 12.30.x | 40 |
No dashboard release matches pg-boss 12.36.x (schema 44). Move the application to 12.37 and use 1.11.x.
The dashboard does not migrate your schema. If the versions do not match, the overview, queue, job and schedule lists and the warnings page still load. However, opening a job, cancelling, resuming or deleting one, sending a job, creating a queue, and adding or removing a schedule all fail with pg-boss database requires migrations. When your application moves to a pg-boss release with a new schema version, upgrade the dashboard with it.
Installation
npm install @pg-boss/dashboardQuick Start
For a quick local test:
DATABASE_URL="postgres://user:password@localhost:5432/mydb" npx pg-boss-dashboardOpen http://localhost:3000 in your browser.
Configuration
The dashboard is configured via environment variables:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string(s) | postgres://localhost/pgboss |
PGBOSS_SCHEMA | pg-boss schema name(s) | pgboss |
PORT | Server port | 3000 |
PGBOSS_DASHBOARD_AUTH_USERNAME | Basic auth username (optional) | - |
PGBOSS_DASHBOARD_AUTH_PASSWORD | Basic auth password (optional) | - |
PGBOSS_DASHBOARD_READ_ONLY | Set to 1 to disable every mutating action (see Read-only mode) | - |
PGBOSS_DASHBOARD_BASE_PATH | Sub-path to serve the dashboard under, e.g. /pgboss (see Serving under a sub-path) | / |
PGBOSS_DASHBOARD_QUERY_TIMEOUT | Max milliseconds per dashboard query before server-side cancellation (statement_timeout). Requires a restart to change. | 60000 |
Basic Authentication
To protect the dashboard with basic authentication, set PGBOSS_DASHBOARD_AUTH_USERNAME and PGBOSS_DASHBOARD_AUTH_PASSWORD:
PGBOSS_DASHBOARD_AUTH_USERNAME=admin \
PGBOSS_DASHBOARD_AUTH_PASSWORD=secret \
DATABASE_URL="postgres://localhost/mydb" \
npx pg-boss-dashboardBoth variables must be provided together. If only one is set, the dashboard will throw an error on startup.
Read-only mode
Set PGBOSS_DASHBOARD_READ_ONLY=1 to serve the dashboard as a viewer:
PGBOSS_DASHBOARD_READ_ONLY=1 \
DATABASE_URL="postgres://localhost/mydb" \
npx pg-boss-dashboardEvery page still loads and every query still runs. What changes:
- The server rejects every non-
GET/HEADrequest with403, so sending, retrying, cancelling, resuming, deleting, creating queues, and scheduling are all refused, including a request crafted by hand. - The controls for those actions are not rendered, and
/send,/queues/create, and/schedules/newexplain themselves instead of showing a form.
This is a global switch rather than a permission system: everyone who can reach the dashboard sees the same read-only view. It is independent of basic authentication and can be combined with it.
Multi-Database Configuration
To monitor multiple pg-boss instances, separate connection strings with a pipe (|):
DATABASE_URL="postgres://host1/db1|postgres://host2/db2" npx pg-boss-dashboardYou can optionally name each database for better identification in the UI:
DATABASE_URL="Production=postgres://prod/db|Staging=postgres://stage/db" npx pg-boss-dashboardIf your databases use different schemas, specify them with matching pipe separation:
DATABASE_URL="postgres://host1/db1|postgres://host2/db2" \
PGBOSS_SCHEMA="pgboss|jobs" \
npx pg-boss-dashboardWhen multiple databases are configured, a database selector appears in the sidebar. The selected database is persisted in the URL via the db query parameter, making it easy to share links to specific database views.
Production Deployment
Option 1: Direct Node.js
npm install @pg-boss/dashboard
DATABASE_URL="postgres://user:pass@localhost:5432/db" \
node node_modules/@pg-boss/dashboard/build/server.jsOption 2: Docker
FROM node:24
WORKDIR /app
RUN npm install -g @pg-boss/dashboard
ENV PORT=3000
EXPOSE 3000
CMD ["pg-boss-dashboard"]docker build -t pgboss-dashboard .
docker run -d \
-e DATABASE_URL="postgres://user:pass@host:5432/db" \
-p 3000:3000 \
pgboss-dashboardOption 3: Docker Compose
services:
dashboard:
image: node:24
working_dir: /app
command: sh -c "npm install -g @pg-boss/dashboard && pg-boss-dashboard"
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
PGBOSS_SCHEMA: pgboss
PORT: 3000
ports:
- "3000:3000"
depends_on:
- dbReverse Proxy
For production, place a reverse proxy in front of any of the above options. Example Nginx configuration:
server {
listen 80;
server_name pgboss.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Serving under a sub-path
By default the dashboard is served from the root path (/). To serve it under a sub-path (for example behind a reverse proxy at https://example.com/pgboss/), set PGBOSS_DASHBOARD_BASE_PATH when you start it:
PGBOSS_DASHBOARD_BASE_PATH=/pgboss npx pg-boss-dashboardAssets, in-app navigation, and action redirects all stay under the prefix. This is a runtime setting, so it works with the published npm package as is; no rebuild is needed. The reverse proxy should forward the prefix unchanged (do not strip it). Match /pgboss rather than /pgboss/: the home page fetches its data at /pgboss.data.
location /pgboss {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Setting
PGBOSS_DASHBOARD_BASE_PATHat build time still works and bakes the prefix into the build. A value set at runtime takes precedence.
The dev server (
npm run dev) always serves from the root path.
Mounting inside an existing application
If you already run a Node.js server, you can mount the dashboard in it instead of running a separate process. createDashboardHandler() returns a standard Fetch API handler, (request: Request) => Promise<Response>.
With a framework built on Request/Response, such as Hono, it plugs straight in:
import { Hono } from 'hono'
import { createDashboardHandler } from '@pg-boss/dashboard/handler'
const dashboard = createDashboardHandler({
databases: [{ url: process.env.DATABASE_URL, schema: 'pgboss' }],
basePath: '/admin/queues',
})
const app = new Hono()
app.use('/admin/*', requireAdmin) // your own authentication middleware
app.all('/admin/queues', (c) => dashboard(c.req.raw))
app.all('/admin/queues.data', (c) => dashboard(c.req.raw)) // the home page's data request
app.all('/admin/queues/*', (c) => dashboard(c.req.raw))With Express, Fastify or plain node:http, convert the request first. @hono/node-server ships an adapter that does it:
import express from 'express'
import { getRequestListener } from '@hono/node-server'
const app = express()
app.use(['/admin/queues', '/admin/queues.data'], requireAdmin, (req, res) => {
req.url = req.originalUrl // Express strips the mount path; the handler needs it
return getRequestListener(dashboard)(req, res)
})| Option | Description |
|---|---|
databases | One or more { url, name?, schema? } entries. The first is selected by default. Give each a distinct name when two connection strings end in the same database name. |
basePath | The path you mount the handler under. Requests must reach the handler with this prefix still in the URL. Defaults to /. |
allowedActionOrigins | Hosts a form may be submitted from, as host[:port] or *.example.com. Needed behind a proxy, see below. |
auth | Put the PGBOSS_DASHBOARD_AUTH_* Basic credential in front of the mounted dashboard. Defaults to false, because the host is normally what authenticates. |
A few things worth knowing:
- Authentication is yours. The handler adds none of its own. Mounted without a guard, it lets anyone read job payloads and send, retry, cancel or delete jobs. Place it behind whatever already protects your admin routes.
- Its forms carry no CSRF token. They rely on React Router's
Origincheck. If your application authenticates with a session cookie, keep your usual CSRF protection on the mounted path, or setPGBOSS_DASHBOARD_READ_ONLY=1. - It is lazy. Nothing is imported and no database connection is opened until the first request arrives. The connection pool drains again when the dashboard is idle.
- It does not depend on the working directory. Static assets are resolved relative to the installed package.
- Route the home page's data request too. React Router fetches it at
<basePath>.data, a sibling of the mount path rather than a child, so a/admin/queues/*pattern alone misses it and the Home link shows a 404. Anything else outside the base path gets a plain 404 from the handler. - Call
close()when your server shuts down. The standalone server exits onSIGTERM/SIGINT; the handler leaves signals to you. A write action starts a pg-boss instance whose timers keep the process alive untilawait dashboard.close(). A closed handler answers 503. - Behind a reverse proxy, list your public host. React Router rejects a form submission whose
Origindoes not match the request URL. When your server sees an internal address such ashttp://127.0.0.1:3000, pages render but every action fails with a 400. PassallowedActionOrigins: ['admin.example.com']. - Environment variables.
PGBOSS_DASHBOARD_READ_ONLYandPGBOSS_DASHBOARD_QUERY_TIMEOUTapply.DATABASE_URL,PGBOSS_SCHEMA,PORT,HOSTandPGBOSS_DASHBOARD_BASE_PATHdo not: the options replace them.PGBOSS_DASHBOARD_AUTH_*is off unless you passauth: true, and if it is set whileauthis off the dashboard says so when you construct the handler rather than ignoring it quietly.
Enabling Warning Persistence
To capture warnings in the dashboard, enable warning persistence in your pg-boss configuration:
const PgBoss = require('pg-boss');
const boss = new PgBoss({
connectionString: 'postgres://localhost/mydb',
persistWarnings: true // Enable warning persistence
});Warnings correlate to warning events already emitted by pg-boss:
slow_query: Queries taking longer than expectedqueue_backlog: Queues exceeding their warning thresholdclock_skew: Database clock drift detectionlisten_notify_unavailable:useListenNotifyis on but no listener could be establishedinvalid_schedule: A stored schedule could not be evaluated and was skippedindex_bloat: A job index holds far more pages than its live entries need and was not rebuiltxmin_horizon: Something is pinning the MVCC horizon, so vacuum reclaims nothingautovacuum_disabled: Nothing is vacuuming a job table at allmonitor_backoff: The queue-stats aggregate was deferred to keep autovacuum moving
A warning type written by a newer pg-boss core than the dashboard is displayed under its raw name.
Tech Stack
- Framework: React Router 8 (framework mode)
- Server: Hono
- Styling: Tailwind CSS v4
- Components: Base UI
- Database: pg (PostgreSQL client)
- Testing: Vitest + Testing Library
Troubleshooting
"Failed to load dashboard"
- Verify
DATABASE_URLis correct and the database is accessible - Ensure the pg-boss schema exists (run pg-boss at least once to create it)
- Check PostgreSQL logs for connection errors
No warnings showing
- Ensure
persistWarnings: trueis set in your pg-boss configuration - Warnings are only recorded after enabling this option
Queue stats seem stale
Queue statistics are cached in the queue table by pg-boss's monitoring system. They update based on your monitorStateIntervalSeconds configuration (default: 30 seconds).
Contributing
To work on the dashboard from source, see the package README.