Events
Each pg-boss instance is an EventEmitter, and contains the following events.
error
The error event could be raised during internal processing, such as scheduling and maintenance. Adding a listener to the error event is strongly encouraged because of the default behavior of Node.
If an EventEmitter does not have at least one listener registered for the 'error' event, and an 'error' event is emitted, the error is thrown, a stack trace is printed, and the Node.js process exits.
Source: Node.js Events > Error Events
Ideally, code similar to the following example would be used after creating your instance, but before start() is called.
boss.on('error', error => logger.error(error));warning
During monitoring and maintenance, pg-boss may raise warning events. The payload contains message and data properties with details about the warning.
boss.on('warning', ({ message, data }) => {
console.log('pg-boss warning:', message, data);
});Warning Types
| Type | Description | Data Properties |
|---|---|---|
slow_query | A maintenance query exceeded the slow query threshold | elapsed (seconds) |
queue_backlog | A queue has exceeded its warning threshold | name, queuedCount, warningQueued |
clock_skew | Database clock is out of sync with application server | seconds, direction |
listen_notify_unavailable | useListenNotify is enabled but a LISTEN/NOTIFY listener could not be established (for example a db adapter without listen, or PgBouncer transaction pooling); pg-boss continues with polling only | type, error |
index_bloat | A job index is holding far more pages than its live entries need and was not rebuilt — because rebuilds are disabled, the connected role does not own the index, the index exceeds maxIndexBytes, or REINDEX CONCURRENTLY failed. The message names the reason. Emitted once per index rather than on every pass, and again if the condition returns after being cleared | name, table, pages, entries, bytes, owned |
xmin_horizon | Something is holding the database's MVCC transaction horizon back — an open transaction, a replication slot, a standby with hot_standby_feedback, or a prepared transaction — so autovacuum cannot reclaim the rows pg-boss deletes and both tables and indexes grow without bound. Fires on measured evidence: a job table past the point Postgres would vacuum it, a vacuum that has since run and reclaimed nothing, and a holder old enough to explain it (see monitorVacuum). The message names the holder class and the table it was measured on. Emitted once per episode rather than on every pass, and again if the horizon is pinned a second time | source, holder, transactions, table, tables, liveTuples, deadTuples, budget, vacuumAgeSeconds, oldestTransactionSeconds, unreadableSources |
autovacuum_disabled | (see monitorVacuum) A job table has autovacuum_enabled = false, holds more dead rows than Postgres's own vacuum point, and is still growing with no vacuum running against it at all. Distinct from xmin_horizon, where vacuum does run and cannot reclaim: here the fix is autovacuum rather than a stuck transaction, so no holder is named. Stays quiet for an operator vacuuming on their own schedule, whose manual vacuum both moves the timestamp and drops the count. Emitted once per episode | table, tables, liveTuples, deadTuples, budget, vacuumAgeSeconds |
monitor_backoff | The queue-stats aggregate spent long enough scanning the job table to risk holding autovacuum back, so the next refresh is deferred (see monitorVacuum). Cached counts are served in the meantime, including to getQueueStats({ force: true }); capturedOn tells you how old they are. Job expiry and heartbeat failure are unaffected. The fix is to shrink the job table (retention, deleteAfterSeconds) or partition its busiest queues | elapsedSeconds, naptimeSeconds, backoffSeconds, backoffUntil |
invalid_schedule | A stored schedule could not be evaluated (for example an unusable timezone written by an older release) and was skipped for this cron pass; the remaining schedules are unaffected. Emitted once per broken schedule rather than on every pass, and again if the schedule is edited or the instance restarts | queue, key, cron, timezone |
Warning Persistence
Warnings are emitted as events by default. To also persist warnings to the database for historical tracking, enable the persistWarnings option:
const boss = new PgBoss({
connectionString: 'postgres://...',
persistWarnings: true
});When enabled, warnings are stored in the warning table and can be queried directly or viewed in the pg-boss dashboard. See SQL for the table schema.
To automatically prune old warnings, set the warningRetentionDays option:
const boss = new PgBoss({
connectionString: 'postgres://...',
persistWarnings: true,
warningRetentionDays: 30 // Auto-delete warnings older than 30 days
});wip
Emitted at most once every 2 seconds whenever at least one worker has an active job. The payload is an array that represents each worker in this instance of pg-boss.
[
{
id: 'fc738fb0-1de5-4947-b138-40d6a790749e',
workId: 'a1b2c3d4-5678-90ab-cdef-1234567890ab',
name: 'my-queue',
options: { pollingInterval: 2000 },
state: 'active',
count: 1,
createdOn: 1620149137015,
lastFetchedOn: 1620149137015,
lastJobStartedOn: 1620149137015,
lastJobEndedOn: null,
lastJobDuration: 343,
lastError: null,
lastErrorOn: null
}
]workId is the value returned by work(). When using localConcurrency, multiple worker entries in the array will share the same workId, allowing you to correlate them back to a specific work() call.
const workId = await boss.work('my-queue', { localConcurrency: 5 }, handler)
boss.on('wip', workers => {
const myWorkers = workers.filter(w => w.workId === workId)
const working = myWorkers.filter(w => w.count > 0).length
const idle = myWorkers.length - working
console.log(`working: ${working}/${myWorkers.length}, idle: ${idle}`)
})stopped
Emitted after stop() once all workers have completed their work and maintenance has been shut down.
bam
Emitted when a boss async migration (BAM) command changes status. BAM commands are database operations that run asynchronously after schema migrations, such as creating indexes on partitioned tables.
boss.on('bam', event => {
console.log(`BAM ${event.name}: ${event.status}`)
})The event payload contains:
{
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'create-index',
status: 'completed', // 'in_progress', 'completed', or 'failed'
queue: 'my-queue', // queue name if applicable
table: 'j1a2b3c4...', // target table name
error: undefined // error message if status is 'failed'
}This event is useful for monitoring migration progress in production environments or for logging purposes.
flow
Emitted by the background flow resolver each time it unblocks one or more dependent jobs (created via flow()) whose parents have completed. See flowIntervalSeconds in the constructor options for how often the resolver runs.
boss.on('flow', event => {
console.log(`Resolved ${event.resolved} flow job(s) in ${event.table}`)
})The event payload contains:
{
table: 'job_common', // partition table the dependents were unblocked in
resolved: 3 // number of dependent jobs unblocked in this batch
}