Broccoli is a Redis-backed Python task queue for running background work with:
- Priority scheduling (strict priority tiers + FIFO ordering inside each tier)
- Dependency-aware tasks (
depends_on) - Retries + dead-letter handling
- Crash/stall recovery
- Multiple worker runtimes (base, threaded, async, hybrid, gpu)
- CLI tooling for operational inspection and control
It is designed for teams that want Celery-like queue behavior with a smaller, explicit codebase.
- Features
- Architecture and Redis data model
- Requirements
- Installation
- Quick start
- Core concepts
- Task lifecycle
- Worker types
- Task dependencies
- Dead-letter and recovery
- CLI reference
- Environment variables
- Programmatic API reference
- Operational guidance
- Troubleshooting
- Development
- License
- Push tasks with explicit priorities (
0is highest priority) - FIFO ordering is preserved within each priority band
- Queue operations are persisted in Redis for durability
- A task can declare
depends_on=[<task_id>, ...]for fan-out/fan-in style waits - Dependent tasks are marked
waitinguntil all listed parents complete - Dependency release is handled automatically on parent completion
- Single-threaded worker (
BaseWorker) - Thread pool worker (
ThreadedWorker) - Asyncio worker (
AsyncWorker) - Hybrid worker (
HybridWorker: async dispatch + threaded execution) - GPU worker (
GPUWorker: hybrid worker pinned to a GPU queue/device)
- Built-in retry handling (
max_retries, default3) - Dead-letter capture for permanently failed tasks
- Requeue dead-letter tasks for replay
- Stalled task recovery (manual and startup auto-recovery)
- Queue depth and processing stats
- Dead-letter inspection CLI commands
- Health checks for Redis reachability
Broccoli primarily uses Redis sorted sets and hashes:
<base>:queue— runnable tasks, scored by(priority tier + FIFO sequence)<base>:processing— in-flight tasks, scored by processing timestamp<base>:sequence— monotonic sequence for FIFO ordering<task_prefix>:<task_id>— task metadata hashdependency:<task_id>— set of blocked dependents waiting for parent<task_prefix>:dead_letter— dead-letter task IDs with failure timestampsdl:<task_id>— dead-letter task snapshotresult:<task_id>— result records with TTL
- Python 3.11+
- Redis 6+
pip install broccoli-workersLocal development install:
pip install -e .from broccoli.core.task.task import Task
from broccoli.core.task.task_queue import TaskQueue
queue = TaskQueue(
redis_url="redis://localhost:6379",
decode_responses=True, # set False to work with raw Redis bytes
)
task = Task(task_type="send_email", payload={"to": "user@example.com"})
queue.push(task, priority=1)
print(task.task_id)from broccoli.workers.threaded_worker import ThreadedWorker
worker = ThreadedWorker(redis_url="redis://localhost:6379", max_workers=4)
@worker.registry.register("send_email")
def send_email(payload):
print(f"Sending email to {payload['to']}")
return {"status": "sent", "to": payload["to"]}
worker.start()Task fields include:
task_id: UUID (auto-generated unless provided)task_type: handler key in registrypayload: arbitrary JSON-serializable dictionarystatus:pending | waiting | in_progress | completed | failedretries: current retry countmax_retries: retry limitdepends_on: optional list of parent task IDsresult: handler outputerror: error message for failed attempts
TaskRegistry is singleton-backed, so handlers are globally visible in-process.
You can register with a decorator:
@worker.registry.register("my_task")
def my_task(payload):
return {"ok": True}Or manually:
worker.registry.register_manually("my_task", my_task_handler)- Push: task metadata hash is saved.
- Queue placement:
- no dependency ->
pendingin runnable queue - with unresolved dependency ->
waiting
- no dependency ->
- Pop: worker moves task to
in_progressand processing set. - Execution:
- success ->
completed - failure + retries left ->
pending(requeued) - failure + retries exhausted ->
failed(dead-letter eligible)
- success ->
- Post-processing:
- results persisted
- task hash cleanup for terminal states
- handlers invoked
- Sequential task execution
- Simple and predictable behavior
- Useful for debugging and low-throughput workloads
- Executes tasks concurrently using
ThreadPoolExecutor - Good for mixed and blocking workloads
- Config:
max_workers
- Uses
asyncioloop with bounded concurrency - Runs task handlers via executor with timeout enforcement
- Config:
max_concurrent
- Async dispatch + thread pool execution
- Suitable for high-throughput mixed workloads
- Config:
thread_workers,async_tasks
- Dedicated worker for GPU workloads (
gpu_tasks:queue) - Pins execution to a selected GPU via
--gpu-id - Uses
HybridWorkerexecution model with GPU cache cleanup
When pushing dependent tasks:
- If all parents already completed, dependent is enqueued immediately.
- If one or more parents are incomplete, dependent enters
waitingand is linked under each unresolveddependency:<parent_id>set.
On parent completion, waiting tasks decrement their remaining dependency count and enqueue only when all dependencies are satisfied.
Helpful APIs:
queue.get_waiting_for(parent_id)queue.get_waiting_tasks()
On permanent failure, task IDs are stored in <task_prefix>:dead_letter and a snapshot is persisted at dl:<task_id>.
queue.requeue_dead(task_id)CLI:
broccoli dead list
broccoli dead requeue <task_id>Manual recovery before startup:
broccoli worker start --type threaded --recover-stalled 600Automatic startup recovery (default enabled):
broccoli worker start --type threaded --recover-on-startup
broccoli worker start --type threaded --no-recover-on-startupbroccoli -v ...
broccoli -vv ...broccoli worker start --type threaded
broccoli worker start --type async --concurrency 20
broccoli worker start --type hybrid --thread-workers 8 --async-tasks 50
broccoli worker start --type gpu --gpu-id 0
broccoli worker start --type threaded --pool --num-workers 4Common worker flags:
--redis-url--queue-name--task-prefix--worker-id--recover-stalled--recover-stalled-timeout--recover-on-startup/--no-recover-on-startup--decode-responses/--no-decode-responses--redis-socket-timeout--redis-socket-connect-timeout--redis-health-check-interval--redis-retry-on-timeout/--no-redis-retry-on-timeout--redis-max-connections
broccoli queue stats --format table
broccoli queue stats --format json
broccoli queue list --status pending --limit 20
broccoli queue get <task_id>
broccoli queue waiting <parent_id>broccoli dead list
broccoli dead list --format json
broccoli dead requeue <task_id>broccoli healthBroccoli CLI defaults can come from environment variables:
BROCCOLI_REDIS_URL(default:redis://localhost:6379)BROCCOLI_QUEUE_NAME(default:tasks:queue)BROCCOLI_TASK_PREFIX(default:task)BROCCOLI_REDIS_DECODE_RESPONSES(default:true)BROCCOLI_REDIS_SOCKET_TIMEOUT(optional, seconds)BROCCOLI_REDIS_SOCKET_CONNECT_TIMEOUT(optional, seconds)BROCCOLI_REDIS_HEALTH_CHECK_INTERVAL(default:30)BROCCOLI_REDIS_RETRY_ON_TIMEOUT(default:true)BROCCOLI_REDIS_MAX_CONNECTIONS(optional)
- constructor:
TaskQueue(redis_url=..., queue_name=..., task_prefix=..., decode_responses=True, redis_config={...}) push(task, priority=0) -> task_idpop() -> Task | Nonecomplete(task)fail(task)requeue(task_id, priority=None)requeue_dead(task_id) -> boolrecover_stalled(timeout_seconds=3600) -> intget_task(task_id) -> Task | Nonestats() -> dictprocessing_stats() -> dictget_waiting_for(task_id) -> list[str]get_waiting_tasks() -> list[str]is_fully_drained() -> bool
All workers support lifecycle hooks:
add_completion_handler(handler)add_failure_handler(handler)add_pre_process_handler(handler)add_post_process_handler(handler)- decorator aliases:
on_complete,on_failure,on_pre_process,on_post_process
Keep task registrations in one importable module and register into each worker instance at startup.
Retries can execute handlers multiple times. Side effects should be safe to repeat.
Choose recover_stalled_timeout high enough to avoid recovering long-running but valid tasks.
A growing dead-letter set often indicates handler bugs or dependency/data issues.
Use distinct queue names/prefixes for high-latency jobs or tenant isolation.
- Ensure tasks are pushed to the same
queue_namethe worker consumes. - Verify
task_prefixalignment between producer and worker. - Confirm handlers are registered for every
task_type.
- Register the handler in the same process that runs the worker.
- Import your registration module before calling
worker.start().
- Check parent task status with
broccoli queue get <parent_id>. - Confirm parent was completed (not failed/deleted).
- Enable startup recovery or run
--recover-stalledmanually.
- Run
broccoli health. - Verify Redis URL/auth/network.
Install development dependencies:
pip install -e .[dev]Run tests:
pytestFormat/lint/type-check tools are configured in pyproject.toml (black, isort, flake8, mypy).
MIT License. See /home/runner/work/Broccoli/Broccoli/LICENSE.