Flask Async Celery
Run async def Celery tasks on a persistent asyncio event loop with bounded concurrency, Flask integration, and Celery consumer backpressure.
Features
- Persistent
asyncioevent loop in a dedicated thread per Celery worker process. - Run native
async defCelery tasks. - Bounded asynchronous concurrency with
max_tasks. - Celery request context propagation into the asyncio execution thread.
self.retry()support for async tasks.- Normal synchronous Celery tasks continue to work.
- Redis consumer-side backpressure through Celery's
worker_disable_prefetch. - Flask extension with simple configuration.
- Graceful asyncio executor shutdown.
- Compatible with Celery 5.6.x and Python 3.10+.
Architecture
Redis
│
▼
Celery Consumer
│
worker_disable_prefetch
│
▼
AsyncIOPool
max_tasks = N
│
▼
AsyncExecutor
asyncio.Semaphore(N)
│
Persistent loop
│
┌────────────┼────────────┐
▼ ▼ ▼
Async task Async task Async task
The package separates Celery's worker execution from asyncio execution:
- Celery receives and traces the task.
AsyncIOPoolbridges Celery execution into the asyncio executor.AsyncExecutorowns a persistent asyncio event loop.- A semaphore limits active async tasks.
- Celery's Redis
worker_disable_prefetchoption can prevent the consumer from reserving work beyond the available pool capacity.
Requirements
- Python 3.10+
- Celery 5.6.x
- Flask 2.3+
- Redis when using the Redis broker/result backend and consumer backpressure.
Installation
From PyPI:
pip install flask-async-celery
For Redis support:
pip install "flask-async-celery[redis]"
For development and testing:
pip install "flask-async-celery[test]"
Basic Flask Setup
import asyncio
from flask import Flask
from flask_async_celery import AsyncCelery, AsyncTask
app = Flask(__name__)
celery = AsyncCelery(
app,
broker_url="redis://127.0.0.1:6379/0",
result_backend="redis://127.0.0.1:6379/1",
max_tasks=5,
disable_prefetch=True,
)
@celery.task(base=AsyncTask)
async def my_task(value):
await asyncio.sleep(1)
return value * 2
Send the task normally:
result = my_task.delay(10)
print(result.get(timeout=30))
# 20
Flask Configuration
Configuration can be supplied through Flask:
app.config["ASYNC_CELERY_MAX_TASKS"] = 10
app.config["ASYNC_CELERY_DISABLE_PREFETCH"] = True
celery = AsyncCelery(
app,
broker_url="redis://127.0.0.1:6379/0",
result_backend="redis://127.0.0.1:6379/1",
)
Available settings:
| Setting | Default | Description |
|---|---|---|
ASYNC_CELERY_MAX_TASKS |
20 |
Maximum number of concurrently executing async tasks. |
ASYNC_CELERY_DISABLE_PREFETCH |
True |
Enables Celery consumer-side backpressure. |
Constructor arguments can also be used directly:
celery = AsyncCelery(
app,
max_tasks=10,
disable_prefetch=True,
)
Flask configuration takes precedence over the constructor defaults when the extension is initialized.
Worker Configuration
Run the worker using the package's custom pool:
celery -A your_app.celery worker \
-P flask_async_celery.pool:AsyncIOPool \
-c 5 \
--loglevel=INFO
For example:
celery -A your_app.celery worker \
-P flask_async_celery.pool:AsyncIOPool \
-c 10 \
--loglevel=INFO
The -c value should match the desired async concurrency.
The custom pool exposes its configured concurrency through num_processes, allowing Celery's consumer to use the same capacity when consumer-side prefetch is disabled.
Concurrency
Set the maximum number of simultaneously executing async tasks with:
celery = AsyncCelery(
app,
max_tasks=5,
)
With:
max_tasks = 5
the asyncio executor allows at most five active coroutines at once.
Additional work waits for an available execution slot.
This is different from simply creating more threads. The package uses one persistent asyncio event loop and runs async coroutines concurrently on that loop.
Consumer Backpressure
For Redis, Celery 5.6 supports:
worker_disable_prefetch = True
The extension enables this by default:
celery = AsyncCelery(
app,
max_tasks=5,
disable_prefetch=True,
)
This provides two levels of protection:
Celery Consumer
│
│ Don't reserve beyond available capacity
▼
AsyncIOPool
│
│ max_tasks
▼
AsyncExecutor
│
│ semaphore
▼
asyncio tasks
You can disable the consumer-side behavior:
celery = AsyncCelery(
app,
max_tasks=5,
disable_prefetch=False,
)
When disabled, the asyncio executor still enforces its own concurrency limit.
Redis Requirement
worker_disable_prefetch is intended for supported Redis worker configurations. If you use another broker, verify that your Celery version and broker transport support this feature before relying on consumer-side backpressure.
The executor-level concurrency limit remains independent of consumer prefetch behavior.
Async Tasks
Use AsyncTask as the base class for native async tasks:
from flask_async_celery import AsyncTask
@celery.task(base=AsyncTask)
async def fetch_data():
await some_async_operation()
return "done"
The task can use normal Celery task features:
@celery.task(
base=AsyncTask,
bind=True,
max_retries=3,
)
async def process_item(self, item_id):
try:
return await process(item_id)
except TemporaryError as exc:
raise self.retry(
exc=exc,
countdown=5,
)
The Celery request context is propagated from the Celery worker thread into the asyncio execution thread, so task information such as the task ID, retry count, delivery information, and retry context remains available.
Synchronous Tasks
Normal synchronous tasks can still be used:
@celery.task
def sync_task(value):
return value * 2
The package does not require every task to be asynchronous.
Retries
Async self.retry() is supported:
@celery.task(
base=AsyncTask,
bind=True,
max_retries=3,
)
async def retrying_task(self):
if should_retry():
raise self.retry(countdown=5)
return "success"
The Celery task request is preserved when execution moves from the Celery worker thread to the asyncio event loop. This allows Celery retry metadata and delivery information to remain available to the async task.
Exceptions
Exceptions raised by an async task propagate through the Celery execution path:
@celery.task(base=AsyncTask)
async def failing_task():
raise RuntimeError("something went wrong")
Celery remains responsible for task failure state, result handling, retry behavior, and worker-level task tracing.
Graceful Shutdown
The asyncio executor runs in a dedicated daemon thread.
When the pool stops, the executor:
- Stops accepting new work.
- Stops the asyncio event loop.
- Cancels pending asyncio tasks.
- Waits for the loop thread to terminate.
- Closes the event loop.
Public API
The main public API is intentionally small:
from flask_async_celery import AsyncCelery, AsyncTask
AsyncCelery
Flask integration and Celery configuration.
AsyncCelery(
app=None,
*,
celery=None,
broker_url=None,
result_backend=None,
max_tasks=20,
disable_prefetch=True,
)
AsyncTask
Base class for asynchronous Celery tasks:
@celery.task(base=AsyncTask)
async def my_task():
...
Development
Clone the repository and install the project in editable mode:
git clone <repository-url>
cd flask-async-celery
pip install -e ".[test]"
Run the test suite:
pytest -v
The test suite covers:
- asyncio executor concurrency
- AsyncIOPool execution
- async task exceptions
- async retries
- synchronous retries
- real Celery worker integration
- Redis consumer backpressure
- Flask extension configuration
- end-to-end Flask/Celery/async execution
Project Structure
flask-async-celery/
├── pyproject.toml
├── README.md
├── src/
│ └── flask_async_celery/
│ ├── __init__.py
│ ├── extension.py
│ ├── executor.py
│ ├── bootstep.py
│ ├── pool.py
│ └── task.py
└── test/
├── conftest.py
├── test_executor.py
├── test_tasks.py
├── test_backpressure.py
├── test_celery_pool.py
├── test_worker_integration.py
├── test_extension.py
└── test_extension_integration.py
Design Notes
This package does not replace Celery's task tracing and lifecycle handling.
Celery remains responsible for:
- task delivery
- task acknowledgment
- retries
- result state
- task IDs
- worker lifecycle
- task tracing
The package provides the asyncio execution layer and integrates it with Celery's pool interface.
The asyncio event loop is persistent for the lifetime of the worker process rather than creating a new event loop for every task.
Why a Persistent Event Loop?
Creating a new event loop for every task adds unnecessary setup and teardown overhead.
Instead, each worker process owns one persistent asyncio event loop:
Celery Worker Process
│
├── Celery Consumer
├── Celery task execution
├── bridge threads
│
└── asyncio event loop thread
├── Task A
├── Task B
└── Task C
Async tasks can therefore share the same event loop while still being bounded by max_tasks.
Why Request Propagation?
Celery's request context is associated with the worker execution context.
The asyncio event loop runs in a separate thread, so the package explicitly transfers the current Celery request into that execution context.
This preserves information required by features such as:
self.request.id
self.request.retries
self.request.delivery_info
self.retry()
The request is pushed before async execution and removed afterward.
Limitations
Broker-specific Backpressure
Consumer-side worker_disable_prefetch support depends on Celery and the broker transport.
The asyncio executor's own max_tasks limit remains the final execution boundary.
Worker Pool
The worker must use:
flask_async_celery.pool:AsyncIOPool
for the package's asyncio execution model.
One Event Loop Per Worker Process
Each worker process owns its own asyncio event loop and concurrency limit.
For example:
celery -A your_app.celery worker \
-P flask_async_celery.pool:AsyncIOPool \
-c 5
creates a worker configuration with five execution slots.
If you run multiple worker processes, each process has its own pool and event loop.
Testing
Run the complete test suite:
pytest -v
The project currently tests the execution model with real Celery workers in addition to unit-level executor and pool tests.
A successful test run should show all tests passing.
Building the Package
Install the build tooling:
python -m pip install build
Build the source distribution and wheel:
python -m build
The generated files will be placed in:
dist/
You can inspect the generated wheel with:
python -m pip install dist/*.whl
Version
Current version:
0.1.0
License
Add the project's license information here.
Release files for flask-async-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 | |
|---|---|---|---|
| flask_async_celery-0.1.0.tar.gz | 16.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flask_async_celery-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.9 kB
Release files / flask_async_celery-0.1.0.tar.gz
| Download URL | flask_async_celery-0.1.0.tar.gz |
|---|---|
| Size | 16.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bcb9e4d39e7fb56d1ac2f9e7d43a880f5eb8767ee205fab7227ac03d8d753f95
|
|
BLAKE2b-256 checksum How to use checksums |
77707c59c88db8f6451ae5078b9f0c259085e019ae50e72d4b16067c08b5410c
|
| 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 27, 2026.
Transparency logRelease files / flask_async_celery-0.1.0-py3-none-any.whl
| Download URL | flask_async_celery-0.1.0-py3-none-any.whl |
|---|---|
| Size | 12.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a7ed3d3d85bface48e37cc632602587bce7bd42805d3fefc9ef51d26bdc61267
|
|
BLAKE2b-256 checksum How to use checksums |
fca51f0e9e9c0354262bb0d9dfa12e23344547b8bfeb3396c4acd6785c2482b5
|
| 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 27, 2026.
Transparency log