SAQ Board
A dashboard to monitor SAQ queues, manage jobs, and control cron schedules.
Inspired by bull-board. It extends SAQ's built-in dashboard and its API is a superset of SAQ's, so it drops in wherever you use saq.web today.
| Queue | Cron jobs |
|---|---|
Features
- Queues: overview with live counts, per-queue tabs for active, queued, scheduled, completed, failed and aborted jobs, plus workers.
- Jobs: rows that expand in place to a timeline and data, result, error and options views, with progress bars, and retry, abort, run now (promote), duplicate and remove actions. Each job also has its own page.
- Bulk actions: retry all, clean all, promote all, abort all.
- Pause and resume a queue.
- Add jobs from the dashboard, picking from the functions your workers register.
- Cron jobs, Sidekiq-cron style: defined in code, then enabled, disabled, enqueued now or deleted from the dashboard, with next run, last enqueued and history.
- Light and dark theme, adjustable refresh, relative or absolute times, local or UTC, read-only mode, Redis stats.
- Preact + htm with no build step, like SAQ's own dashboard. Fonts and scripts ship with the package, so it works offline.
Install
pip install "saq-board[starlette]" # Starlette / FastAPI
pip install "saq-board[web]" # aiohttp and the saq-board command
Redis queues only for now.
1. Add the plugin to your workers
SAQ doesn't keep finished jobs, can't pause a queue and schedules cron jobs in code only. The plugin adds all three. Wrap your worker settings:
from saq import Queue
from saq_board import CronJob, with_board
queue = Queue.from_url("redis://localhost")
settings = with_board({
"queue": queue,
"functions": [send_email],
"cron_jobs": [
CronJob(cleanup, cron="0 * * * *", description="Hourly cleanup"),
],
})
Run workers exactly as before: saq app.worker.settings.
with_board(settings, history_limit=1000, cron_poll=1.0):
history_limit: finished jobs kept per status (complete, failed, aborted). The oldest are dropped first.cron_poll: seconds between cron checks.
2. Run the dashboard
Starlette / FastAPI, a drop-in for saq.web.starlette.saq_web:
from starlette.routing import Mount
from saq_board import saq_board
routes = [Mount("/monitor", saq_board("/monitor", queues=[queue]))]
# FastAPI: app.mount("/monitor", saq_board("/monitor", queues=[queue]))
aiohttp, a drop-in for saq.web.aiohttp.create_app:
from saq_board import create_app
app.add_subapp("/monitor", create_app([queue], root_path="/monitor"))
On its own:
saq-board --url redis://localhost:6379 # queues registered by the plugin
saq-board --url redis://localhost:6379 -q default -q emails
saq-board --settings app.worker.settings --port 8080
All of them take read_only=True (--read-only), which hides every action and rejects them server-side.
Authentication
Same as SAQ's dashboard. The aiohttp app and the saq-board command turn on basic auth when AUTH_PASSWORD is set (user AUTH_USER, default admin); this needs aiohttp_basicauth, which the web extra installs. With Starlette or FastAPI, protect the mount with your own middleware or dependencies.
Cron jobs
Cron jobs work like Sidekiq-cron:
- They are defined in code (
cron_jobsin the worker settings) and synced to Redis when a worker starts. - The dashboard enables, disables, enqueues now or deletes them. Enabled or disabled survives restarts; a deleted job comes back the next time a worker that defines it starts.
- Every worker process checks the schedule, and a claim in Redis makes sure each run is enqueued once.
- A run that is more than 60 seconds late (for example, no worker was up) is skipped, not caught up.
unique=True(the default, as in SAQ) skips a run while the previous one is still queued or running.- Times follow the worker's
cron_tz(UTC by default).
saq_board.CronJob is SAQ's CronJob plus name, description and enabled (the initial state). Plain saq.CronJob works too and is named after its function. Give jobs that share a function different names.
API
Everything SAQ's dashboard serves, plus:
| Method | Path | |
|---|---|---|
| GET | /api/queues |
Queues with counts, totals, paused flag and workers |
| GET | /api/queues/{queue} |
One queue, as in SAQ, plus the registered functions |
| GET | /api/queues/{queue}/jobs?status=&offset=&limit= |
Jobs by status |
| POST | /api/queues/{queue}/jobs |
Add a job: {"function", "kwargs", "options"} |
| GET | /api/queues/{queue}/jobs/{job} |
One job |
| POST | /api/queues/{queue}/jobs/{job}/{action} |
retry, abort, promote, remove |
| POST | /api/queues/{queue}/{action} |
pause, resume, promote-all, and with {"status"}: clean, retry-all, abort-all |
| GET | /api/redis |
Redis stats |
| GET | /api/cron |
Cron jobs |
| GET | /api/cron/{queue}/{name} |
One cron job with its history |
| POST | /api/cron/{queue}/{name}/{action} |
enqueue, enable, disable, delete |
| POST | /api/cron/{action}-all |
The same for every cron job |
How it works
The plugin wraps the worker's queue: finish also records the job in a capped history, and dequeue waits while the queue is paused (a pause takes effect within a second). It also moves cron_jobs to its own scheduler, so SAQ's scheduler doesn't run them too, and tags the worker's metadata so the dashboard can tell whether workers run the plugin.
Board data lives under each queue's SAQ namespace, saq:<queue>:board:*, so it goes away with the queue.
Mutation safety
Retry and abort each run as one Redis script that first checks the job is still what the dashboard showed, so a double click, two open tabs or a busy queue can't corrupt it.
- Retry requeues only a finished job, and only the run you are looking at. If the key now belongs to a newer or running job, or another retry got there first, it returns 409.
- Abort finishes a queued or scheduled job at once. A running job stays aborting until its worker acknowledges cancellation. Repeated abort requests are idempotent; retry and enqueue with the same key remain blocked during cancellation cleanup.
- Pending aborts appear in the active tab and persist in
board:aborting, outside stock SAQ's sweep list. The incomplete entry still prevents duplicate enqueue. If a worker disappears, the request stays pending and visible for operator investigation. A second abort does not prove worker death or force completion; recovery must verify that the old execution has stopped before marking it terminal. - While a worker is picking a job up, before it marks it active, abort returns 409. Refresh and try again.
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]" uvicorn
redis-server --daemonize yes
.venv/bin/pytest # uses redis://localhost:6379/15, override with SAQ_BOARD_TEST_REDIS
.venv/bin/python examples/demo.py # http://localhost:8000/monitor
Credits
Built by rkwap.
SAQ Board stands on the shoulders of:
- SAQ by Toby Mao (MIT): the queue this extends, whose dashboard API, structure and
#1095c1accent it keeps. - bull-board by felixmosh (MIT): the dashboard design it follows.
- Sidekiq-cron by ondrejbartas and contributors (MIT): how cron jobs behave and look.
- Preact (MIT) and htm (Apache-2.0) by Jason Miller, Hugeicons free icons (MIT), and the Geist fonts (SIL OFL 1.1), all bundled. See THIRD_PARTY_NOTICES.md.
License
MIT, like SAQ and bull-board.
Metadata
Release files for saq-board 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| saq_board-0.1.4.tar.gz | 113.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| saq_board-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 235.4 kB
Release files / saq_board-0.1.4.tar.gz
| Download URL | saq_board-0.1.4.tar.gz |
|---|---|
| Size | 113.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e3e2cf8dcfba58d0f2940387e47dd8a5cdebe154001a028b3584978e241b0970
|
|
BLAKE2b-256 checksum How to use checksums |
5b07bc095dbbc13ed5c80c93ffb9344df9221c6a58d95b7588b63744534b98df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency logRelease files / saq_board-0.1.4-py3-none-any.whl
| Download URL | saq_board-0.1.4-py3-none-any.whl |
|---|---|
| Size | 121.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
da4642d15b92eba3bff0f63b8c1e1c1b179a7434a741dfbbbec734bce2ebd61a
|
|
BLAKE2b-256 checksum How to use checksums |
3294b683a32caf7e8d3f03e8e1999efd55107c97a1e060c81a35425ddabd7845
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.
Transparency log