janbjorge/pgqueuer

PostgreSQL-backed background job and task queue for Python.

Python

1,536

804 commits

updated Oct 12, 2026

See the code

README

πŸš€ PgQueuer: PostgreSQL-backed background job and task queue for Python

CI pypi downloads stars versions

πŸ“š Docs Β· πŸ’» Source Β· πŸ’¬ Discord

Your PostgreSQL database is already a job queue.

PgQueuer turns PostgreSQL into a fast, reliable background job processor. Jobs live in the same database as your application data. One stack, full ACID guarantees, and no separate message broker to run.

AI-assisted setup

Already using a coding agent? Copy the onboarding prompt from the PgQueuer documentation. The prompt directs your agent to the maintained setup route and canonical docs.

Features

  • πŸ’‘ Minimal footprint: one pip install; bring your existing PostgreSQL connection and start enqueueing
  • πŸ” Transactional enqueue: commit a job in the same transaction as your data; no dual-write drift
  • βš›οΈ Safe concurrency: workers claim jobs with FOR UPDATE SKIP LOCKED (never double-processed), with per-entrypoint limits and serialized dispatch when you need them
  • πŸš€ Instant dispatch: LISTEN/NOTIFY wakes workers the moment a job lands (with a polling fallback)
  • ⏰ Scheduling & deferral: cron-style recurring tasks and execute_after, no separate scheduler process
  • πŸ“Š Observability: completion tracking, Prometheus metrics, tracing (Logfire/Sentry), and a live dashboard
  • πŸ§ͺ In-memory mode: run the whole queue without Postgres for tests and prototyping

Why PostgreSQL?

If you already run PostgreSQL, it can do double duty as your job queue. That means one fewer service to operate, and your queue and data stay consistent because they share the same database and transactions.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  enqueue   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Your App │───────────▢│            │──────────▢│ Worker 1 │──┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚            β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                        β”‚ PostgreSQL β”‚  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                        β”‚            │──────────▢│ Worker 2 │───
                        β”‚            β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                        β”‚            β”‚  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                        β”‚            │──────────▢│ Worker N │───
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β–²  FOR UPDATE SKIP LOCKED         β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Installation

PgQueuer targets Python 3.10+ and PostgreSQL 14+:

pip install pgqueuer
pgq install        # create tables and functions in your database

The CLI reads PGHOST, PGUSER, PGDATABASE and related environment variables. Use pgq sql install to preview the SQL (or pipe it to another client: pgq sql install | psql), --prefix myapp_ to namespace tables, or pgq uninstall to remove the schema.

Quick Start

PgQueuer pairs consumers (workers that process jobs) with producers (code that enqueues jobs).

1. Define a consumer

Each entrypoint is a job handler. Run it with the CLI: pgq run examples.consumer:main.

import asyncpg
from pgqueuer import PgQueuer
from pgqueuer.db import AsyncpgDriver
from pgqueuer.models import Job

async def main() -> PgQueuer:
    connection = await asyncpg.connect()
    pgq = PgQueuer(AsyncpgDriver(connection))

    @pgq.entrypoint("fetch")
    async def process(job: Job) -> None:
        print(f"Processed: {job!r}")

    return pgq

2. Enqueue jobs

From your web app, script, or anywhere else with a database connection:

import asyncpg
from pgqueuer.db import AsyncpgDriver
from pgqueuer.queries import Queries

async def main() -> None:
    connection = await asyncpg.connect()
    queries = Queries(AsyncpgDriver(connection))
    await queries.enqueue("fetch", b"hello world")

The job arrives instantly via LISTEN/NOTIFY, and your consumer's process handler picks it up.

Enqueue inside a transaction

This is what a database-backed queue buys you: the job and your business data commit together, or not at all.

order_id = 42

async with connection.transaction():
    await connection.execute(
        "INSERT INTO orders (id, status) VALUES ($1, 'paid')", order_id
    )
    await queries.enqueue("send_receipt", str(order_id).encode())
    # If the transaction rolls back, the job is never enqueued.

Run without a database

PgQueuer.in_memory() is a drop-in replacement that implements the same ports as the real backend, so your handlers stay identical. Good for unit tests and prototyping.

import asyncio
from pgqueuer import PgQueuer
from pgqueuer.models import Job
from pgqueuer.domain.types import QueueExecutionMode

async def main() -> None:
    pq = PgQueuer.in_memory()

    @pq.entrypoint("send_email")
    async def send_email(job: Job) -> None:
        print(f"Sending: {job.payload!r}")

    await pq.qm.queries.enqueue(["send_email"], [b"alice"], [0])
    await pq.qm.run(mode=QueueExecutionMode.drain)

asyncio.run(main())

The in-memory adapter has no durability or multi-process coordination, so use the PostgreSQL backend for production. See the in-memory reference.

Documentation

TopicWhat's inside
Core conceptsConsumers, producers, entrypoints, the job lifecycle
SchedulingCron-style recurring tasks and deferred execution
Concurrency controlPer-entrypoint limits and serialized dispatch
Completion trackingWait for jobs to finish with CompletionWatcher
Shared resourcesInject DB pools, HTTP clients, and models into handlers
Custom executorsRetry strategies and exponential backoff
Driversasyncpg, psycopg async/sync: choosing and configuring
ArchitecturePorts & adapters, SKIP LOCKED, design decisions
ObservabilityPrometheus metrics, tracing, and the dashboard
Framework integrationFastAPI (example) and Flask (example)

Monitor your queues

Launch the interactive dashboard to watch queue activity in real time:

pgq dashboard --interval 10 --limit 25
+---------------------------+-------+------------+--------------------------+------------+----------+
|          Created          | Count | Entrypoint | Time in Queue (HH:MM:SS) |   Status   | Priority |
+---------------------------+-------+------------+--------------------------+------------+----------+
| 2024-05-05 16:44:26+00:00 |  49   |    sync    |         0:00:01          | successful |    0     |
| 2024-05-05 16:44:27+00:00 |  12   |   fetch    |         0:00:03          | queued     |    0     |
| 2024-05-05 16:44:28+00:00 |   3   |  api_call  |         0:00:00          | picked     |    5     |
+---------------------------+-------+------------+--------------------------+------------+----------+

Development

PgQueuer uses Testcontainers to spin up an ephemeral PostgreSQL instance for the test suite. Just have Docker running.

uv sync --frozen  # install dependencies
uv run ruff check . && uv run ruff format . --check && uv run lint-imports && uv run mypy . && uv run pytest  # full check suite

License

PgQueuer is MIT licensed. See LICENSE for details.

asyncio
background-jobs
job-queue
postgres
postgresql
python
queue
task-queue

janbjorge/pgqueuer

PostgreSQL-backed background job and task queue for Python.

Python

1,536

804 commits

updated Oct 12, 2026

See the code

README

πŸš€ PgQueuer: PostgreSQL-backed background job and task queue for Python

CI pypi downloads stars versions

πŸ“š Docs Β· πŸ’» Source Β· πŸ’¬ Discord

Your PostgreSQL database is already a job queue.

PgQueuer turns PostgreSQL into a fast, reliable background job processor. Jobs live in the same database as your application data. One stack, full ACID guarantees, and no separate message broker to run.

AI-assisted setup

Already using a coding agent? Copy the onboarding prompt from the PgQueuer documentation. The prompt directs your agent to the maintained setup route and canonical docs.

Features

  • πŸ’‘ Minimal footprint: one pip install; bring your existing PostgreSQL connection and start enqueueing
  • πŸ” Transactional enqueue: commit a job in the same transaction as your data; no dual-write drift
  • βš›οΈ Safe concurrency: workers claim jobs with FOR UPDATE SKIP LOCKED (never double-processed), with per-entrypoint limits and serialized dispatch when you need them
  • πŸš€ Instant dispatch: LISTEN/NOTIFY wakes workers the moment a job lands (with a polling fallback)
  • ⏰ Scheduling & deferral: cron-style recurring tasks and execute_after, no separate scheduler process
  • πŸ“Š Observability: completion tracking, Prometheus metrics, tracing (Logfire/Sentry), and a live dashboard
  • πŸ§ͺ In-memory mode: run the whole queue without Postgres for tests and prototyping

Why PostgreSQL?

If you already run PostgreSQL, it can do double duty as your job queue. That means one fewer service to operate, and your queue and data stay consistent because they share the same database and transactions.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  enqueue   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Your App │───────────▢│            │──────────▢│ Worker 1 │──┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚            β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                        β”‚ PostgreSQL β”‚  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                        β”‚            │──────────▢│ Worker 2 │───
                        β”‚            β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                        β”‚            β”‚  NOTIFY   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                        β”‚            │──────────▢│ Worker N │───
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β–²  FOR UPDATE SKIP LOCKED         β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Installation

PgQueuer targets Python 3.10+ and PostgreSQL 14+:

pip install pgqueuer
pgq install        # create tables and functions in your database

The CLI reads PGHOST, PGUSER, PGDATABASE and related environment variables. Use pgq sql install to preview the SQL (or pipe it to another client: pgq sql install | psql), --prefix myapp_ to namespace tables, or pgq uninstall to remove the schema.

Quick Start

PgQueuer pairs consumers (workers that process jobs) with producers (code that enqueues jobs).

1. Define a consumer

Each entrypoint is a job handler. Run it with the CLI: pgq run examples.consumer:main.

import asyncpg
from pgqueuer import PgQueuer
from pgqueuer.db import AsyncpgDriver
from pgqueuer.models import Job

async def main() -> PgQueuer:
    connection = await asyncpg.connect()
    pgq = PgQueuer(AsyncpgDriver(connection))

    @pgq.entrypoint("fetch")
    async def process(job: Job) -> None:
        print(f"Processed: {job!r}")

    return pgq

2. Enqueue jobs

From your web app, script, or anywhere else with a database connection:

import asyncpg
from pgqueuer.db import AsyncpgDriver
from pgqueuer.queries import Queries

async def main() -> None:
    connection = await asyncpg.connect()
    queries = Queries(AsyncpgDriver(connection))
    await queries.enqueue("fetch", b"hello world")

The job arrives instantly via LISTEN/NOTIFY, and your consumer's process handler picks it up.

Enqueue inside a transaction

This is what a database-backed queue buys you: the job and your business data commit together, or not at all.

order_id = 42

async with connection.transaction():
    await connection.execute(
        "INSERT INTO orders (id, status) VALUES ($1, 'paid')", order_id
    )
    await queries.enqueue("send_receipt", str(order_id).encode())
    # If the transaction rolls back, the job is never enqueued.

Run without a database

PgQueuer.in_memory() is a drop-in replacement that implements the same ports as the real backend, so your handlers stay identical. Good for unit tests and prototyping.

import asyncio
from pgqueuer import PgQueuer
from pgqueuer.models import Job
from pgqueuer.domain.types import QueueExecutionMode

async def main() -> None:
    pq = PgQueuer.in_memory()

    @pq.entrypoint("send_email")
    async def send_email(job: Job) -> None:
        print(f"Sending: {job.payload!r}")

    await pq.qm.queries.enqueue(["send_email"], [b"alice"], [0])
    await pq.qm.run(mode=QueueExecutionMode.drain)

asyncio.run(main())

The in-memory adapter has no durability or multi-process coordination, so use the PostgreSQL backend for production. See the in-memory reference.

Documentation

TopicWhat's inside
Core conceptsConsumers, producers, entrypoints, the job lifecycle
SchedulingCron-style recurring tasks and deferred execution
Concurrency controlPer-entrypoint limits and serialized dispatch
Completion trackingWait for jobs to finish with CompletionWatcher
Shared resourcesInject DB pools, HTTP clients, and models into handlers
Custom executorsRetry strategies and exponential backoff
Driversasyncpg, psycopg async/sync: choosing and configuring
ArchitecturePorts & adapters, SKIP LOCKED, design decisions
ObservabilityPrometheus metrics, tracing, and the dashboard
Framework integrationFastAPI (example) and Flask (example)

Monitor your queues

Launch the interactive dashboard to watch queue activity in real time:

pgq dashboard --interval 10 --limit 25
+---------------------------+-------+------------+--------------------------+------------+----------+
|          Created          | Count | Entrypoint | Time in Queue (HH:MM:SS) |   Status   | Priority |
+---------------------------+-------+------------+--------------------------+------------+----------+
| 2024-05-05 16:44:26+00:00 |  49   |    sync    |         0:00:01          | successful |    0     |
| 2024-05-05 16:44:27+00:00 |  12   |   fetch    |         0:00:03          | queued     |    0     |
| 2024-05-05 16:44:28+00:00 |   3   |  api_call  |         0:00:00          | picked     |    5     |
+---------------------------+-------+------------+--------------------------+------------+----------+

Development

PgQueuer uses Testcontainers to spin up an ephemeral PostgreSQL instance for the test suite. Just have Docker running.

uv sync --frozen  # install dependencies
uv run ruff check . && uv run ruff format . --check && uv run lint-imports && uv run mypy . && uv run pytest  # full check suite

License

PgQueuer is MIT licensed. See LICENSE for details.

asyncio
background-jobs
job-queue
postgres
postgresql
python
queue
task-queue

Project health

Latest release

1.6.0 Β· Oct 11, 2026

Releases this year

20

Significant stargazers

Jan Katins

122 followers Β· starred Jun 2024

Terin Stock

455 followers Β· starred Aug 2024

Adam Lindberg

201 followers Β· starred Aug 2024

Justin Clift

363 followers Β· starred Aug 2024