Registry / workflow / saq
library0.26.4pypypiunverified

SAQ is a distributed Python job queue designed for asynchronous applications, leveraging `asyncio` and Redis. It provides a simple, fast, and reliable way to manage background tasks with features like job retries, timeouts, and scheduled execution. The library is actively maintained with frequent updates, though without a strict release cadence.

pip install saq
INSTALL
IMPORT
SIG · SAQ
S
saq
workflowpythonv0.26.4
Install
1.8s avg
Import
Disk
18MB
Pass rate
10/ 10
Env Coverage10 / 10
glibc
3.93.13
musl
3.93.13
Install & Compatibility
Where this runs
tested against v0.26.4 · pip install
no network on importno background threads
Install × environment matrix
Each cell = how many times install + import succeeded across repeated harness runs. Partial = flaky.
glibc = Debian/Ubuntu slim · musl = Alpine Linux
musl
py 3.103.910 runs
installs and imports cleanly · install 0.0s · import 0.000s · 19.5MB
glibc
py 3.103.910 runs
installs and imports cleanly · install 1.8s · import 0.000s · 20MB
18MB installed
● package 18MB
Code
Verified usage

Verified import paths — ran on the pinned version, not inferred.

SAQ
from saq import SAQ
import saq

This quickstart demonstrates how to define an asynchronous task, enqueue it using a SAQ client (producer), and process it with a SAQ worker (consumer). It's designed to run in a single script for demonstration purposes, but in production, the producer and consumer typically run in separate processes. Ensure a Redis server is running and accessible at the specified URL (default: `redis://localhost:6379`). You can run a Redis server quickly using Docker.

import asyncio from saq import Queue, Worker from saq.utils import job import os # Define a task function @job async def my_task(ctx, a, b): """A sample task that adds two numbers.""" print(f"[{ctx.job.id}] Running my_task with {a} + {b}") await asyncio.sleep(0.5) # Simulate async work result = a + b print(f"[{ctx.job.id}] Task finished, result: {result}") return result async def producer(): """Enqueues jobs.""" redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379') q = Queue.from_url(redis_url, name="default") print("Enqueuing jobs...") job1 = await q.enqueue("my_task", a=1, b=2) job2 = await q.enqueue("my_task", a=10, b=20, op="add") # 'op' will be stored in job.meta await asyncio.sleep(0.1) # Give time for jobs to be pushed to Redis print(f"Enqueued job1 ID: {job1.id}, job2 ID: {job2.id}") # Retrieving result blocks until job is done. For quickstart, it's illustrative. # In a real app, you might check results later or not block. if job1: try: job1_result = await job1.result(timeout=2) print(f"Job1 result (producer-side): {job1_result}") except asyncio.TimeoutError: print("Job1 result timed out on producer side.") async def consumer(): """Runs the worker to process jobs.""" redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379') q = Queue.from_url(redis_url, name="default") worker = Worker( queue=q, functions=[my_task], # Register the task function concurrency=1 # For simple quickstart, use 1 concurrent task ) print("Starting worker for 5 seconds...") try: # In a real application, worker.start() would run indefinitely. # For a quickstart, we'll run it briefly and then stop. await asyncio.wait_for(worker.start(), timeout=5) except asyncio.TimeoutError: print("Worker stopped due to timeout (expected for quickstart).") except asyncio.CancelledError: print("Worker cancelled.") finally: await worker.stop() # Ensure clean shutdown async def main(): print("This quickstart demonstrates SAQ client (producer) and worker (consumer) in a single script.") print("In a real scenario, these would typically run in separate processes.") print("Ensure a Redis server is running at redis://localhost:6379 or set the REDIS_URL environment variable.") print("Example: `docker run -p 6379:6379 --name saq-redis -d redis/redis-stack:latest`") await asyncio.gather(producer(), consumer()) print("\nQuickstart finished. Check Redis for any remaining jobs if the worker didn't process them all.") if __name__ == "__main__": asyncio.run(main())
Debug
Known issues
breakingThe `Worker` constructor now requires the `functions` argument to explicitly register task functions. Previously, tasks could be registered using `worker.register_job()` after instantiation.
fix
Initialize `Worker` with `Worker(queue=q, functions=[my_task_1, my_task_2])` instead of registering functions separately. This makes task registration explicit during worker setup.
affects: >=0.20.0
gotchaWhen using the `@job` decorator without a specific `queue` argument (e.g., `@job`), tasks are implicitly assigned to the 'default' queue. This can lead to tasks being processed by unintended workers if multiple queues are in use.
fix
Always specify the target queue explicitly using `@job(queue='my_specific_queue')` for clarity and to ensure tasks are routed correctly.
affects: All versions
gotchaSAQ's default serializer (JSON) cannot handle complex Python objects (e.g., dataclasses, Pydantic models, custom classes) directly as job arguments. This will cause serialization errors.
fix
Either pass only JSON-serializable arguments (basic types, lists, dicts), or configure a custom serializer like `saq.serializers.PydanticSerializer` (if `pydantic` is installed) or `saq.serializers.MsgpackSerializer` when initializing the `Queue`.
affects: All versions
Upgrade
Version history
0.26.4latest on PyPI · released May 21, 2026
Audit
Dependencies
redisrequiredSAQ uses Redis as its backend for queue management, job storage, and coordination.
pydanticoptionalOptional dependency for enhanced job argument validation and serialization when using `PydanticSerializer`.
Agent activity
29 hits · last 30 days
node
24
OpenAI (training)
1
Resources
saq — pip install saq · libregistry