Scheduling

August 20, 2026 · View on GitHub

PgQueuer includes a built-in scheduler for managing recurring tasks using cron-like expressions. No separate process (like celery-beat) is required.

Basic usage

from pgqueuer.models import Schedule

@pgq.schedule("fetch_db", "* * * * *")
async def fetch_db(schedule: Schedule) -> None:
    await perform_task()

Use a 6-field expression to schedule work at second-level granularity:

@pgq.schedule("heartbeat", "* * * * * */3")
async def heartbeat(schedule: Schedule) -> None:
    await send_heartbeat()

How it works

The @schedule decorator registers a name and a cron expression as a row in pgqueuer_schedules. The scheduler polls that table, runs the tasks whose expressions are due, and records the run. Because the state lives in PostgreSQL, schedules survive process restarts and only one worker runs each due task.

Scheduler flow diagram

@pgq.schedule ──▶ Schedule in DB ──▶ Poll loop ──cron ready──▶ Execute task
                                        ▲                          │
                                        └──────────────────────────┘

Cron expression format

PgQueuer supports both standard 5-field cron expressions and 6-field expressions with seconds in the final position.

5-field format

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Examples:

ExpressionMeaning
* * * * *Every minute
*/5 * * * *Every 5 minutes
0 * * * *Every hour
0 9 * * 1Every Monday at 9:00 AM
0 0 1 * *First day of each month at midnight

6-field format

For second-level schedules, the seconds field comes last:

PgQueuer uses croniter to parse cron expressions. Second-level schedules follow croniter's about second repeats behavior.

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │ ┌───────────── second (0-59)
│ │ │ │ │ │
* * * * * *

Examples:

ExpressionMeaning
* * * * * */3Every 3 seconds
* * * * * */10Every 10 seconds
* * * * * 0At second 0 of every minute

Use trailing seconds, not leading seconds. For example, */3 * * * * * is not "every 3 seconds"; use * * * * * */3 instead.

Accessing shared resources

Scheduled tasks can access shared resources (HTTP clients, database pools, etc.) by annotating a parameter as ScheduleContext. PgQueuer auto-detects it and injects the context, whose .resources mapping mirrors the one passed to PgQueuer:

from pgqueuer.models import Schedule, ScheduleContext

@pgq.schedule("sync_data", "0 * * * *")
async def sync_data(schedule: Schedule, ctx: ScheduleContext) -> None:
    db = ctx.resources["db_pool"]
    await db.execute("SELECT 1")

See Shared Resources for full details.

Cleaning up old schedules

When clean_old=True, PgQueuer deletes any existing database schedules whose entrypoint matches this decorator's entrypoint before re-registering it. This is useful when you change the cron expression for a task and want the old schedule row replaced on startup.

@pgq.schedule("fetch_db", "* * * * *", clean_old=True)
async def fetch_db(schedule: Schedule) -> None:
    await perform_task()

By default, clean_old=False: old schedules are retained.

Managing schedules via CLI

List all schedules:

pgq schedules

Remove a schedule by name:

pgq schedules --remove fetch_db

Example: full setup with scheduling

from contextlib import asynccontextmanager
from datetime import datetime
import asyncpg
from pgqueuer import PgQueuer
from pgqueuer.models import Job, Schedule

@asynccontextmanager
async def main():
    connection = await asyncpg.connect()
    pgq = PgQueuer.from_asyncpg_connection(connection)

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

    @pgq.schedule("hourly_report", "0 * * * *")
    async def hourly_report(schedule: Schedule) -> None:
        print(f"Generating report at {datetime.now()}")

    @pgq.schedule("heartbeat", "* * * * * */3")
    async def heartbeat(schedule: Schedule) -> None:
        print(f"Heartbeat at {datetime.now()}")

    @pgq.schedule("daily_cleanup", "0 2 * * *", clean_old=True)
    async def daily_cleanup(schedule: Schedule) -> None:
        print(f"Running cleanup at {datetime.now()}")

    yield pgq
pgq run myapp:main