Skip to main content

pulsare

Task scheduling without infrastructure. One file. Zero deps.

pip install pulsare

Why pulsare?

Feature schedule APScheduler celery beat pulsare
Cron expressions - + + +
Async support - + - +
Concurrent execution - + + +
Retries + backoff - - + +
Timeouts - - - +
Persistence - + + +
Zero dependencies + - - +
Single file + - - +

Quick start

from pulsare import Scheduler, every, cron, once

s = Scheduler()

@s.job(every(30).seconds, retries=3, timeout=10)
def health_check():
    ping_service()

@s.job(every().day.at("09:00"), missed="run_once")
def daily_report():
    send_report()

@s.job(cron("*/5 * * * *"), max_instances=2)
def rotate_cache():
    do_rotation()

s.run()  # blocking loop (Ctrl+C to stop)

Fluent interval API

every(5).seconds
every(2).minutes
every(1).hours
every(3).days
every().day.at("09:00")
every().monday.at("08:30")
every().friday.at("17:00:00")  # HH:MM:SS supported

Cron expressions

Standard 5-field cron: minute hour day-of-month month day-of-week

cron("*/5 * * * *")     # every 5 minutes
cron("0 9 * * 1-5")     # weekdays at 9am
cron("30 2 1 * *")      # 1st of month at 2:30am
cron("0 0 29 2 *")      # Feb 29 only (leap years)

One-shot tasks

once(after=30)   # run once, 30 seconds from now
once(after=0)    # run once, immediately

Concurrency

s = Scheduler(max_workers=4)

@s.job(every(10).seconds, max_instances=2)
def my_task():
    slow_operation()  # up to 2 running in parallel

Retries with backoff

@s.job(every(1).minutes, retries=3, retry_delay=2, retry_backoff=2)
def flaky_task():
    call_external_api()  # retries at 2s, 4s, 8s

Timeouts

@s.job(every(1).minutes, timeout=30, max_consecutive_timeouts=3)
def risky_task(cancel_event=None):
    while not cancel_event.is_set():  # cooperative cancellation
        do_chunk()

Jobs that exceed max_consecutive_timeouts are automatically paused.

Lifecycle hooks

@s.job(
    every(5).minutes,
    on_success=lambda name, val, dur: log(f"{name} ok in {dur:.1f}s"),
    on_failure=lambda name, exc, dur: alert(f"{name} failed: {exc}"),
    on_timeout=lambda name, dur: alert(f"{name} timed out"),
)
def monitored_task():
    return process_data()

Metrics

m = s.get_metrics("my_task")
# {'success_count': 42, 'fail_count': 1, 'timeout_count': 0,
#  'retry_count': 3, 'total_runs': 43, 'avg_duration': 1.23, ...}

all_m = s.all_metrics()  # dict of all jobs

Tags, pause, cancel

j = s.add(my_fn, every(1).minutes, tags={"api", "critical"})

s.pause("api")              # pause all jobs tagged "api"
s.resume("api")             # resume them
s.cancel("api")             # cancel all tagged "api"
s.cancel_by_name("my_fn")   # cancel by name
s.purge()                   # remove cancelled jobs

Persistence (SQLite)

from pulsare import Scheduler, SQLiteStore, every

store = SQLiteStore("jobs.db")  # WAL mode, zero config
s = Scheduler(store=store)

@s.job(every(5).minutes)
def my_task():
    do_work()

s.run()  # state survives restarts

Persists: next_run, metrics, paused flag. Jobs matched by name on restart.

Async

import asyncio
from pulsare import Scheduler, every

s = Scheduler()

@s.job(every(10).seconds, timeout=5)
async def async_task():
    async with aiohttp.ClientSession() as session:
        await session.get("https://api.example.com")

asyncio.run(s.run_async())  # native async loop

Async jobs share a persistent event loop -- connection pools, caches, etc. work correctly.

Missed job policies

from pulsare import MissedPolicy

@s.job(every(1).hours, missed="skip")      # default: reschedule to next slot
@s.job(every(1).hours, missed="run_once")  # execute once, then resume
@s.job(every(1).hours, missed="run_all")   # catch up all missed intervals

Timezone

from zoneinfo import ZoneInfo

s = Scheduler(tz=ZoneInfo("America/New_York"))
# All jobs use this timezone for .at() and cron expressions

System timezone detected automatically (Windows registry, /etc/localtime, $TZ).

Requirements

  • Python >= 3.10
  • Zero external dependencies

License

MIT

Metadata

Release files for pulsare 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pulsare 1.0.0
File Size Uploaded
pulsare-1.0.0.tar.gz 23.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pulsare 1.0.0
File Interpreter ABI Platform
pulsare-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.7 kB

Release files / pulsare-1.0.0.tar.gz

Download URL pulsare-1.0.0.tar.gz
Size 23.1 kB
Tags Source
SHA-256 checksum
How to use checksums
be66e3c95089656ed4f7b6fd712197a206e8694d7ec6c592740068fae4776b93
BLAKE2b-256 checksum
How to use checksums
3c4b50f2e8cdd447a2abefd2d76f00ca52a5893868883a80a1eb8b33a93c4f3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Release files / pulsare-1.0.0-py3-none-any.whl

Download URL pulsare-1.0.0-py3-none-any.whl
Size 24.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe1a4ead994a90967e0f682bfc98877357e81cf66a4ae7af36770b5113617575
BLAKE2b-256 checksum
How to use checksums
81875314b4f75d51c384f4550941436c7a16dcd85a6b038481af3812c20434c1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.2

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page