Skip to content

Getting started

Install

pip install firm-queue            # or: uv add firm-queue
# add a database driver as needed (SQLite needs nothing):
pip install "firm-queue[postgres]"   # psycopg
pip install "firm-queue[mysql]"      # PyMySQL

See Installation for the full list of extras.

1. Configure

Point the library at a database. This is process-global; call it once at startup.

import firm.queue as bq

bq.configure(database_url="sqlite:///queue.db")
# or "postgresql://localhost/myapp", or "mysql://user:pw@localhost/myapp"

2. Create the schema

For development, create the tables directly:

from firm.queue import schema
from firm.queue import current_runtime

schema.create_all(current_runtime().engine)

For production, use the bundled Alembic migration instead (alembic upgrade head) — see Database backends.

3. Define a job

@bq.job turns a plain function into something you can enqueue. It stays directly callable, so you can unit-test the body without a database.

@bq.job(queue="mailers", priority=5)
def send_welcome(user_id: int, locale: str = "en") -> None:
    user = load_user(user_id)
    deliver_welcome_email(user, locale)

4. Enqueue

from datetime import datetime, timedelta

send_welcome.enqueue(42)                          # run as soon as a worker is free
send_welcome.enqueue(42, locale="fr")             # keyword args work too
send_welcome.enqueue_in(timedelta(hours=1), 42)   # run later
send_welcome.enqueue_at(datetime(2026, 7, 1, 9, 0), 42)  # run at a specific time (naive UTC)

Arguments are stored as JSON. Plain JSON types plus datetime, date, Decimal, and UUID round-trip; anything else raises at enqueue time (see Defining jobs).

5. Run it

Quick, in-process (great for a test)

from firm.queue.worker import run_ready

send_welcome.enqueue(42)
processed = run_ready(current_runtime())   # claim + run up to `limit` (default 10) ready jobs inline

The real thing — a worker process

firm-queue start \
  --database-url sqlite:///queue.db \
  --import myapp.jobs            # import the module(s) that define your @job functions

start runs a supervisor with a worker (thread pool) and a dispatcher (so scheduled jobs fire). Press Ctrl-C for a graceful drain. See the CLI and Workers & the supervisor for all the options.

Full example

# myapp/jobs.py
from datetime import timedelta
import firm.queue as bq

bq.configure(database_url="sqlite:///queue.db")

_sent: list[int] = []

@bq.job()
def greet(user_id: int) -> None:
    _sent.append(user_id)

if __name__ == "__main__":
    from firm.queue import schema
    from firm.queue import current_runtime
    from firm.queue.worker import run_ready

    schema.create_all(current_runtime().engine)
    greet.enqueue(1)
    greet.enqueue(2)
    print("processed:", run_ready(current_runtime()))   # -> processed: 2
    print("sent:", _sent)                                # -> sent: [1, 2]