faststream-celery
A FastStream broker that speaks Celery.
Write async FastStream handlers that consume the tasks your Celery producers already send, and publish tasks your Celery workers already run. Both sides keep working while you move services across one at a time — nothing has to be migrated in a big bang, and neither side needs to know the other changed.
from faststream import FastStream
from faststream_celery import CeleryBroker
broker = CeleryBroker("amqp://guest:guest@localhost:5672//")
app = FastStream(broker)
@broker.subscriber("celery", task="proj.tasks.send_email")
async def send_email(args: list, kwargs: dict) -> str:
return "sent"
That handler now serves app.send_task("proj.tasks.send_email", ...) from any
Celery client, result included.
Install
pip install faststream-celery
Extras, all optional:
pip install "faststream-celery[redis]" # Redis transport and result backend
pip install "faststream-celery[fastapi]" # FastAPI router
pip install "faststream-celery[otel]" # OpenTelemetry tracing
pip install "faststream-celery[prometheus]" # Prometheus metrics
Consuming tasks
task= picks the Celery task a handler serves. Several handlers share one
queue, the way Celery routes:
@broker.subscriber("celery", task="proj.tasks.send_email")
async def send_email(args: list, kwargs: dict) -> None: ...
@broker.subscriber("celery", task="proj.tasks.resize")
async def resize(args: list, kwargs: dict) -> None: ...
@broker.subscriber("celery") # anything the two above do not claim
async def fallback(args: list, kwargs: dict) -> None: ...
Leave task= off and the handler takes every task on the queue. A task nobody
claims is rejected, the way a Celery worker rejects one it does not know.
Returning a value is enough to answer the caller: with an AMQP reply_to or a
result backend configured, AsyncResult.get() on the client side returns it. A
handler that raises comes back as FAILURE with a traceback.
Publishing tasks
from faststream_celery import CeleryTask
await broker.publish(
CeleryTask("proj.tasks.send_email", args=[user_id], kwargs={"urgent": True}),
queue="celery",
)
Deferred execution works as in apply_async:
await broker.publish(
CeleryTask("proj.tasks.digest", countdown=60, expires=3600),
queue="celery",
)
Deferred tasks are held in memory until they are due, and dropped once their
expires has passed.
To wait for a result:
response = await broker.request(CeleryTask("proj.tasks.add", args=[2, 3]), queue="celery")
result = await response.decode() # {"status": "SUCCESS", "result": 5, ...}
Without a result backend that waits on a temporary AMQP reply queue. With one it
reads celery-task-meta-<id>, exactly as AsyncResult.get() does.
Transports and results
CeleryBroker(
"redis://localhost:6379/0",
result_backend="redis://localhost:6379/0",
transport_options={"visibility_timeout": 3600},
)
RabbitMQ and Redis are both supported, and both are tested against a live Celery
on every commit. On Redis, visibility_timeout has to match across every client
and worker sharing the bus — otherwise one side redelivers what another is still
working on.
| Read | Publish | |
|---|---|---|
| Protocol v2 (Celery 4+) | yes | yes |
| Protocol v1 | yes | no |
Messages are JSON, as Celery does by default.
Testing
TestCeleryBroker runs handlers in memory — middlewares, parsing and dependency
injection included — with no broker and no connection:
from faststream_celery import CeleryTask, TestCeleryBroker
async def test_send_email() -> None:
async with TestCeleryBroker(broker):
await broker.publish(CeleryTask("proj.tasks.send_email", args=[1]), queue="celery")
send_email.mock.assert_called_once_with({"args": [1], "kwargs": {}})
Assert inside the block: the mocks are reset on the way out.
A broker configured with result_backend= works there too — the backend is
swapped for an in-memory one, so results are recorded and request() reads
them back with no Redis to run:
from faststream_celery.backend import InMemoryResultBackend
async def test_the_result_is_recorded() -> None:
async with TestCeleryBroker(broker):
response = await broker.request(
CeleryTask("proj.tasks.add", args=[2, 3]),
queue="celery",
correlation_id="task-id-1",
)
envelope = await response.decode()
assert envelope["status"] == "SUCCESS"
backend = broker.config.broker_config.result_backend
assert isinstance(backend, InMemoryResultBackend)
assert backend.results["task-id-1"]["result"] == 5
FastAPI
from fastapi import FastAPI
from faststream_celery.fastapi import CeleryRouter
router = CeleryRouter("amqp://guest:guest@localhost:5672//")
@router.subscriber("celery", task="proj.tasks.add")
async def add(args: list[int], kwargs: dict) -> int:
return sum(args)
app = FastAPI(lifespan=router.lifespan_context)
app.include_router(router)
Observability
from faststream_celery.opentelemetry import CeleryTelemetryMiddleware
from faststream_celery.prometheus import CeleryPrometheusMiddleware
broker = CeleryBroker(
"amqp://guest:guest@localhost:5672//",
middlewares=(
CeleryTelemetryMiddleware(),
CeleryPrometheusMiddleware(registry=registry),
),
)
Spans carry the Celery task name and id, so two tasks on one queue stay apart in a trace. Metrics are labelled by queue; add the task name where you want the two apart there too:
CeleryPrometheusMiddleware(
registry=registry,
custom_labels={"task": lambda msg: (msg.message.headers or {}).get("task", "")},
)
faststream docs gen produces an AsyncAPI schema with the queue, exchange and
routing key of every subscriber and publisher.
Annotations
from faststream_celery.annotations import CeleryMessage, Logger
@broker.subscriber("celery", task="proj.tasks.add")
async def add(message: CeleryMessage, logger: Logger) -> None:
logger.info("running %s", message.headers["task"])
Examples
examples/ has one directory per scenario, with both sides
present — a FastStream app and the stock Celery process it talks to — so you
can start them and watch the tasks cross:
01_celery_to_faststream/ |
a Celery client's tasks served by FastStream handlers |
02_faststream_to_celery/ |
tasks we publish, run by a real celery worker |
03_both_ways/ |
one call crossing the boundary four times |
04_payload_validation/ |
three ways to validate a task payload with Pydantic |
05_fastapi/ |
HTTP routes and task handlers in one app |
| … | canvas, results, scheduling, routers, observability, testing, security |
Contributing
uv sync
uv run pytest tests/unit # fast, no broker needed
uv run pytest tests/integration -m connected # live RabbitMQ, Redis and Celery (needs Docker)
uv run nox # every Python, both ends of the dependency ranges
uv run nox -s lint # ruff and mypy
CI tests pinned minimum and latest released FastStream/Kombu versions across Python 3.10–3.14. The latest-release combination also runs daily at 04:23 UTC:
uv run nox -s tests -k 'faststream_latest and kombu_latest'
AGENTS.md has the development guidelines and the justfile the day-to-day
commands.
License
Apache-2.0.
Metadata
Release files for faststream-celery 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| faststream_celery-0.1.0.tar.gz | 306.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| faststream_celery-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 380.1 kB
Release files / faststream_celery-0.1.0.tar.gz
| Download URL | faststream_celery-0.1.0.tar.gz |
|---|---|
| Size | 306.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ef20cf94f0dbd8387b22670827118b6d145571c48eedaf711712f5c1cfe19c42
|
|
BLAKE2b-256 checksum How to use checksums |
3e06d13fb6e5ca4673e73b7796956e0bedcd8e6249721b4808632195031aff70
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / faststream_celery-0.1.0-py3-none-any.whl
| Download URL | faststream_celery-0.1.0-py3-none-any.whl |
|---|---|
| Size | 73.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e5ee44e8d3d17e8acd59ca104dd6d36aab65f6d248b961054411e7fb627c7334
|
|
BLAKE2b-256 checksum How to use checksums |
35af22ed0c9010406ea8b6387e6498885c29d408b4c458e4a2734f01b65ca567
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.12 {"installer":{"name":"uv","version":"0.12.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|