Django Async Backend
🚀 Installation & Django Integration
1. Install the package
pip install django-async-backend[binary]
The binary extra installs the C-accelerated psycopg implementation. Without it you get the pure-Python implementation, which is noticeably slower.
If you use connection pooling, add the pool extra as well:
pip install django-async-backend[binary,pool]
The current packages depend heavily on the Django version, because a large part of them is autogenerated. The goal is to stay in sync with Django's major and minor versions, for example 6.0.x, tracking whatever Django version is in use.
2. Django settings
In your settings.py, set the database engine to use the async backend
DATABASES = {
"default": {
"ENGINE": "django_async_backend.db.backends.postgresql",
...
},
}
Make sure your Django app (and any other required apps) are listed in INSTALLED_APPS
INSTALLED_APPS = [
...
"django_async_backend",
...
]
Connection Pooling
⚠️ Connection pooling is not supported when running under a WSGI server (including the Django development server), because WSGI creates a new event loop for each request. This prevents reliable management of connection pool state. To disable the warning, set
ASYNC_BACKEND_DISABLE_POOL_WARNING=True
Middleware
When running under ASGI, add close_async_connections to MIDDLEWARE so connections are returned to the pool at the end of each request. Django's request_finished signal only closes sync connections.
MIDDLEWARE = [
"django_async_backend.middleware.close_async_connections",
...
]
Concurrency model
⚠️ Async does not mean parallel here. Database queries are not run in parallel by default.
Within a single async context (such as one request or task), all async ORM and
cursor calls share one connection per database alias. Because they share a
connection, queries are serialized — running them under asyncio.gather() does
not execute them on the database concurrently; they take turns on the shared
connection.
This is a deliberate design choice that mirrors Django's DEP 0009. To run
queries truly in parallel, you must opt in explicitly and give each one its own
connection with async_connections._independent_connection(). See the
DEP 0009 section below for details and an example.
Connection Handler
The connection handler manages database connections for your async backend.
from django_async_backend.db import async_connections
connection = async_connections['default']
async with await connection.cursor() as cursor:
await cursor.execute("SELECT ...")
rows = await cursor.fetchall()
await connection.close()
- Connections are reused and managed automatically.
- Use await connection.close() to manually close a connection if needed.
Cursor
Async cursors provide the following methods:
executeexecutemanyfetchonefetchmanyfetchall
async with await connection.cursor() as cursor:
await cursor.execute("SELECT 1")
row = await cursor.fetchone()
Async Transactions with async_atomic
Basic Usage
Use async_atomic to run async database operations atomically.
All changes inside the block are committed together; if an error occurs, all changes are rolled back.
from django_async_backend.db.transaction import async_atomic
async with async_atomic():
await create_instance(1)
# If no error, changes are committed
# If error, changes are rolled back
Rollback on Error
If an exception is raised inside the block, all changes are rolled back:
async with async_atomic():
await create_instance(1)
raise Exception("fail") # Nothing is committed
Nested Transactions (Savepoints)
You can nest async_atomic blocks. Each inner block creates a savepoint. If an error occurs in the inner block, only its changes are rolled back; outer changes remain.
async with async_atomic():
await create_instance(1)
try:
async with async_atomic():
await create_instance(2)
raise Exception("fail inner") # Only instance 2 is rolled back
except Exception:
pass
# Only instance 1 is in the database
Using on_commit with async transactions
You can register a callback to run after a successful transaction commit using connection.on_commit.
connection = async_connections[DEFAULT_DB_ALIAS]
async with async_atomic():
await connection.on_commit(callback)
Writing Async Tests
AsyncioTestCase
Use for async tests that do not require database transactions.
from django_async_backend.test import AsyncioTestCase
class MyAsyncTests(AsyncioTestCase):
async def asyncSetUp(self):
# Setup code
async def asyncTearDown(self):
# Cleanup code
async def test_something(self):
# Your async test logic
await do_async_stuff()
AsyncioTransactionTestCase
Use for async tests that need database transaction support (rollbacks, atomic blocks).
from django_async_backend.test import AsyncioTransactionTestCase
class MyTransactionTests(AsyncioTransactionTestCase):
async def asyncSetUp(self):
# Setup database
async def asyncTearDown(self):
# Cleanup database
async def test_something(self):
async with async_atomic():
# DB operations here
await do_db_stuff()
ORM support:
AsyncModelMixin
The recommended way to add async ORM support to a model is to inherit from
AsyncModelMixin. The mixin gives every model two things without any extra
boilerplate:
- an
async_objectsmanager (anAsyncManager), so you don't have to declare one by hand; async_save()andasync_delete()methods for saving and deleting instances asynchronously.
from django.db import models, DEFAULT_DB_ALIAS
from django_async_backend.db import async_connections
from django_async_backend.db.models.base import AsyncModelMixin
class Book(AsyncModelMixin, models.Model):
name = models.CharField(max_length=100)
async def main():
# create / save an instance
book = Book(name="Django")
await book.async_save()
# update via update_fields
book.name = "Django Async"
await book.async_save(update_fields=["name"])
# query through the async_objects manager
async for i in Book.async_objects.all():
print(i.id, i.name)
# delete a single instance, or a whole queryset
await book.async_delete()
await Book.async_objects.filter(name="Django Async").adelete()
await async_connections[DEFAULT_DB_ALIAS].close()
async_save() accepts the same keyword arguments as Django's save()
(force_insert, force_update, using, update_fields) and honors model
Meta options such as select_on_save and order_with_respect_to, as well as
multi-table inheritance.
async_delete() accepts the same keyword arguments as Django's delete()
(using, keep_parents), returns the (count, {label: count}) pair, and
cascades through related objects, sending pre_delete / post_delete along
the way. on_delete handlers are resolved to async equivalents, so the standard
CASCADE, PROTECT, RESTRICT, SET_NULL, SET_DEFAULT, SET(...) and
DO_NOTHING all work; a custom synchronous on_delete callable is
rejected with a TypeError, because it would run a blocking query.
Manager:
If you prefer, you can attach an AsyncManager to a model explicitly instead of
using AsyncModelMixin:
from django.db import models, DEFAULT_DB_ALIAS
from django_async_backend.db import async_connections
from django_async_backend.db.models.manager import AsyncManager
class Book(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
async_objects = AsyncManager()
async def main():
async for i in Book.async_objects.all():
print(i.id)
await async_connections[DEFAULT_DB_ALIAS].close()
| methods | supported | comments |
|---|---|---|
Model.objects.aget |
✅ | |
Model.objects.acreate |
✅ | |
Model.objects.acount |
✅ | |
Model.objects.none |
✅ | |
Model.objects.abulk_create |
✅ | |
Model.objects.abulk_update |
✅ | |
Model.objects.aget_or_create |
✅ | |
Model.objects.aupdate_or_create |
✅ | |
Model.objects.aearliest |
✅ | |
Model.objects.alatest |
✅ | |
Model.objects.afirst |
✅ | |
Model.objects.alast |
✅ | |
Model.objects.ain_bulk |
✅ | |
Model.objects.adelete |
✅ | |
Model.objects.aupdate |
✅ | |
Model.objects.aexists |
✅ | |
Model.objects.acontains |
✅ | |
Model.objects.aexplain |
✅ | |
Model.objects.araw |
❌ | |
Model.objects.all |
✅ | |
Model.objects.filter |
✅ | |
Model.objects.exclude |
✅ | |
Model.objects.complex_filter |
✅ | |
Model.objects.union |
✅ | |
Model.objects.intersection |
✅ | |
Model.objects.difference |
✅ | |
Model.objects.select_related |
❌ | |
Model.objects.select_for_update |
✅ | |
Model.objects.prefetch_related |
❌ | |
Model.objects.aaggregate |
❌ | |
Model.objects.annotate |
✅ | |
Model.objects.order_by |
✅ | |
Model.objects.distinct |
✅ | |
Model.objects.extra |
✅ | |
Model.objects.reverse |
✅ | |
Model.objects.defer |
⚠️ | not safe for async, will not be implemented — use values/values_list |
Model.objects.only |
⚠️ | not safe for async, will not be implemented — use values/values_list |
Model.objects.using |
✅ | |
Model.objects.resolve_expression |
✅ | |
Model.objects.ordered |
✅ | |
Model.objects.values |
✅ | |
Model.objects.values_list |
✅ | |
Model.objects.dates |
✅ | |
Model.objects.datetimes |
✅ | |
Model.objects.alias |
✅ | |
__aiter__ |
✅ | |
__iter__ |
⚠️ | raises TypeError — use async for obj in qs |
__len__ |
⚠️ | raises TypeError — use await qs.acount() |
__contains__ |
⚠️ | falls back to __iter__, so it raises TypeError too |
__bool__ |
⚠️ | truth-testing falls back to __len__, so if qs: raises TypeError — use await qs.aexists() |
__repr__ |
✅ | |
__and__ |
✅ | |
__or__ |
✅ | |
__xor__ |
✅ | |
__getitem__ |
✅ | |
Model.objects.aiterator |
❌ |
RawQuerySet
Not supported ❌
Model:
| methods | supported | comments |
|---|---|---|
Model.asave |
✅ | async_save |
Model.adelete |
✅ | async_delete |
Model.arefresh_from_db |
❌ |
Code generation
Part of the ORM layer is generated, not hand-written. The async versions of
Django's query classes are produced from Django's own source by the codemon
tool (under codemon/), which rewrites the sync code into async using libcst.
These files are committed to git (so the package installs without running
codegen) and track a specific Django version, so they look hand-written but are
not. Each starts with a # This file was generated automatically. Do not modify it manually. header.
To change them, edit the codemon config under codemon/config/*.yaml — not
the generated files — then regenerate:
lets generate
This restores the Django-derived files to pristine, runs python -m codemon,
and reformats the result — running codemon on its own skips the
restore/reformat and produces large formatting-only diffs. Regeneration
downloads Django's source for the pinned version over the network, so it needs
internet access. Any manual edits to the generated modules will be lost on the
next regeneration.
⚙️ Development Setup
Install pre-commit hooks:
pip install pre-commit
pre-commit install
Install dependencies:
poetry install --with dev
🧪 Running Tests
This project uses a comprehensive test suite powered by unittest.
To run tests:
docker-compose up postgres -d
DJANGO_SETTINGS_MODULE=settings poetry run python -m unittest discover -s tests
Integration tests run locally.
The django_async_backend.db.backends.postgresql backend is fully compatible with Django's default django.db.backends.postgresql backend, as it leverages the default implementation under the hood. To confirm this compatibility, run Django's test suite using the custom backend.
DATABASES = {
"default": {
"ENGINE": "django_async_backend.db.backends.postgresql",
...
},
"other": {
"ENGINE": "django_async_backend.db.backends.postgresql",
...
},
}
To execute them:
cd tests_django
docker-compose run --build --rm test_django_integration
DEP 0009
https://github.com/django/deps/blob/main/accepted/0009-async.rst
Whenever a new_connections() block is entered, Django sets a new context with new database connections.
To show show an example how it might looks like with a current implementation we have the independent_connection context manager.
import asyncio
from django_async_backend.db import async_connections
async def run_query():
async with async_connections._independent_connection():
conn = await async_connections['default']
async with conn.cursor() as cursor:
await cursor.execute("SELECT ...")
return await cursor.fetchall()
results = await asyncio.gather(run_query(), run_query(), run_query())
It's just a concept that is not ready for production usage.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_async_backend-6.0.8.tar.gz.
File metadata
- Download URL: django_async_backend-6.0.8.tar.gz
- Upload date:
- Size: 119.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.1.1 CPython/3.12.13 Linux/6.17.0-1020-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
074a17102027ec372ad3b9b37dcc1e1454be528591785739cb7e14333f6bed4f
|
|
| MD5 |
1c9038b0b1829b394ba8c052bb25c52e
|
|
| BLAKE2b-256 |
5a8be6b12108cb3bd20199bb12819579f777b447cf2b107fbebd346f31db29c6
|
File details
Details for the file django_async_backend-6.0.8-py3-none-any.whl.
File metadata
- Download URL: django_async_backend-6.0.8-py3-none-any.whl
- Upload date:
- Size: 127.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.1.1 CPython/3.12.13 Linux/6.17.0-1020-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6ae3fbe3450a39694a54d1783cf66ee27b8fb04bfc42b27161f8c1e4469f54b
|
|
| MD5 |
782f2ab4bc52ff48cf0f797720da49f2
|
|
| BLAKE2b-256 |
30bff059a0f154064838f6e765274101a1d4804922d447935609ad595ea53456
|