A PostgreSQL-based async job scheduler with deduplication, periodic jobs, and reliability features
Project description
PG Scheduler
A simple lightweight async first job scheduler for Python that uses PostgreSQL to allow you to schedule and manage the execution of asynchronous tasks.
It's heavily inspired by APScheduler in its API but horizontally scalable and much more focused in the features it provides and technologies it uses.
It makes minimal assumptions about how you run it other than that you are in an asyncIO enviroment, use Postgresql and asyncpg. Due to this it can integrate very easily with other technologies.
This library is focused on easy to use and deploy scheduler and not being an all comprehensive job runner so if you need something more advanced and complicated like workers queues and worker producer patterns I believe these are already very well covered in the python ecosystem and I would suggest you to look at those instead.
Features
- Periodic jobs via
@periodicdecorator (interval or cron expressions) - Timezone support for cron schedules using
zoneinfo - Cross-node deduplication — exactly-once execution across replicas sharing the same database
- Priority queues — CRITICAL, HIGH, NORMAL, LOW
- Retry logic with exponential backoff
- Advisory locks for exclusive execution when needed
- Vacuum policies for automatic cleanup of completed/failed jobs
- Reliability — graceful shutdown, heartbeat monitoring, orphan recovery, atomic job claiming
Installation
pip install pg-scheduler
Requires Python 3.9+, PostgreSQL 12+, asyncpg, and croniter.
Quick Start
One-off Job
import asyncio
import asyncpg
from datetime import datetime, timedelta, UTC
from pg_scheduler import Scheduler, JobPriority
async def send_email(recipient: str, subject: str):
print(f"Sending email to {recipient}: {subject}")
await asyncio.sleep(1)
async def main():
db_pool = await asyncpg.create_pool(
user='scheduler', password='password',
database='scheduler_db', host='localhost'
)
scheduler = Scheduler(db_pool=db_pool, max_concurrent_jobs=10)
await scheduler.start()
try:
job_id = await scheduler.schedule(
send_email,
execution_time=datetime.now(UTC) + timedelta(minutes=5),
args=("user@example.com", "Welcome!"),
priority=JobPriority.NORMAL,
max_retries=3
)
print(f"Scheduled job: {job_id}")
await asyncio.sleep(300)
finally:
await scheduler.shutdown()
await db_pool.close()
if __name__ == "__main__":
asyncio.run(main())
Periodic Jobs
Interval-based
from datetime import timedelta
from pg_scheduler import periodic, JobPriority
@periodic(every=timedelta(minutes=15))
async def cleanup_temp_files():
...
@periodic(every=timedelta(hours=1), priority=JobPriority.CRITICAL, max_retries=3)
async def generate_hourly_report():
...
Cron-based
@periodic(cron="0 0 * * *")
async def daily_backup():
...
@periodic(cron="0 3 * * SUN", timezone="America/New_York")
async def weekly_report():
...
@periodic(cron="0 9-17 * * MON-FRI", timezone="Europe/London", priority=JobPriority.HIGH)
async def business_hours_task():
...
Advisory Locks
For operations that must run on exactly one node (e.g., database migrations):
@periodic(every=timedelta(minutes=30), use_advisory_lock=True)
async def exclusive_maintenance():
...
Most jobs don't need this — the built-in deduplication already prevents duplicate executions.
Decorator Parameters
@periodic(
every=timedelta(minutes=15), # Interval (or use cron= instead)
cron="*/15 * * * *", # Cron expression (alternative to every=)
timezone="UTC", # Timezone for cron (string or ZoneInfo)
use_advisory_lock=False, # Exclusive execution
priority=JobPriority.NORMAL, # CRITICAL, HIGH, NORMAL, LOW
max_retries=0, # Retry attempts on failure
job_name=None, # Custom name (auto-generated if None)
enabled=True # Whether job is active
)
Cross-Node Deduplication
Multiple nodes running the same code automatically deduplicate — no configuration needed:
@periodic(every=timedelta(minutes=5))
async def cleanup_task():
...
# Node 1: Schedules job for 10:05 -> succeeds
# Node 2: Tries to schedule same job -> "Already exists, ignoring"
# Node 3: Tries to schedule same job -> "Already exists, ignoring"
# Result: Only one node executes cleanup at 10:05
Management API
scheduler.get_periodic_jobs() # List all periodic jobs
scheduler.get_periodic_job_status(dedup_key) # Status of a specific job
scheduler.enable_periodic_job(dedup_key) # Enable a job
scheduler.disable_periodic_job(dedup_key) # Disable a job
await scheduler.trigger_periodic_job(dedup_key) # Trigger immediately
Conflict Resolution
Handle duplicate job IDs:
ConflictResolution.RAISE(default) — error on duplicateConflictResolution.IGNORE— keep existing, return its IDConflictResolution.REPLACE— update existing with new parameters
Vacuum Policies
Automatic cleanup of old jobs:
from pg_scheduler import VacuumConfig, VacuumPolicy
vacuum_config = VacuumConfig(
completed=VacuumPolicy.after_days(1),
failed=VacuumPolicy.after_days(7),
cancelled=VacuumPolicy.after_days(3),
interval_minutes=60,
track_metrics=True
)
scheduler = Scheduler(db_pool, vacuum_config=vacuum_config)
Configuration
scheduler = Scheduler(
db_pool=db_pool,
max_concurrent_jobs=25, # Max concurrent job execution
misfire_grace_time=300, # Seconds before pending jobs expire (None to disable)
vacuum_enabled=True, # Enable automatic job cleanup
vacuum_config=vacuum_config # Custom vacuum policies
)
Contributing
Contributions are welcome. Please feel free to submit a Pull Request.
License
MIT — see LICENSE.
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pg_scheduler-0.2.2.tar.gz.
File metadata
- Download URL: pg_scheduler-0.2.2.tar.gz
- Upload date:
- Size: 24.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36241b3299a79d0fdf080b211acfe74feb5980c1a0019d31bb203f968e707f2d
|
|
| MD5 |
1e2fba7093a6e1e021bc30efa7303646
|
|
| BLAKE2b-256 |
054f7ddb390c98f8e56c6bd7d38ba892b475e4e51bba8a31561fb5805990e515
|
File details
Details for the file pg_scheduler-0.2.2-py3-none-any.whl.
File metadata
- Download URL: pg_scheduler-0.2.2-py3-none-any.whl
- Upload date:
- Size: 28.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7db0f2f2b64daff47d17234484240a51d21f29b8385a57008c11a2704fc1237
|
|
| MD5 |
cbe0dd51f02524983e8ef6afc2e3a8d4
|
|
| BLAKE2b-256 |
4a2f3f17a3d8f71a6c5b9f74f01d8ce4858388afe586a9b90b76a00b1a71d34c
|