2024-11-23 19:40:00
jonathanadly.com
We have traditionally used Django in all our products. We believe it is one of the most underrated, beautifully designed, rock solid framework out there.
However, if we are to be honest, the history of async usage in Django wasn’t very impressive. It was always clunky, and you end up with your code cluttered with this ugly syntax.
async_to_sync(some_function)(arg_1, arg_2)
You could argue that for most products, you don’t really need async. It was just an extra layer of complexity without any significant practical benefit.
Over the last couple of years, AI use-cases have changed that perception. Many AI products have calling external APIs over the network as their bottleneck. This makes the complexity from async Python worth considering. FastAPI with its intuitive async usage and simplicity have risen to be the default API/web layer for AI projects.
We watched with concern as the view of Django as a clunky async framework spread. This happened partly because large language models had outdated information and partly because there aren’t any complex or large open-source projects showcasing Django’s async usage.
💡
Async code does not improve the performance of CPU-bound tasks. It enhances performance in areas where tasks are waiting for IO to complete by allowing the CPU to handle other tasks in the meantime. Again, AI use-cases are a great candidate for this as the bottleneck is usually waiting on data.
One of our side quests when building ColiVara was to demonstrate Django’s async features and to be completely end-to-end async. We aimed to be an open-source project that could effectively showcase Django’s async capabilities.
As background, ColiVara is a retrieval API that allows you to store, search, and retrieve documents based on their visual embeddings. It works exactly like RAG from the end-user standpoint – but using vision models instead of chunking and text-processing for documents.
It is designed as a Django API service with a separate, standalone GPU service that handles the AI workloads.
The key takeaway is that your application needs full async support to truly benefit from it. It’s a commitment you make from the start, and mixing sync and async code won’t give you any benefits. It will only add unnecessary complexity.
If your code is a mix between sync and async, Django has to mimic the other call style to make your code work. This switch causes a slight performance delay of about a millisecond. Is it a big deal? Probably not if you have a sync call occasionally. However, if your application is a 50/50 mix, it’s likely better to stick with sync code.
In summary, this means you must:
ASGI Web Server
For a Django project, the usual choices are either Daphne or Uvicorn. We think both have similar capabilities. We chose Uvicorn because it’s lighter and more like “gunicorn.” Our general rule is to use Daphne if we’re working with WebSockets and Django Channels; otherwise, we go with Uvicorn.
Async views
We used django-ninja as our API library. It is newer than the well-known Django Rest Framework and focuses on performance and support for async code.
Our experience with django-ninja has been rock solid, and we recommend starting new API Django projects with it, even if you aren’t writing async views or endpoints.
The coding style is very similar to FastAPI, but you also get all the Django features that make it “batteries-included.”
Async ORM
The biggest improvement in Django over the last six months is how much the ORM now supports async code. Practically, the vast majority of operations are supported. You simply add a prefix of a
and things work out of the box.
User.objects.get(id=1)
User.objects.aget(id=1)
One thing to be mindful of, is all the LLMs as of late 2024 are completely outdated on what is possible and what is not possible with Django async ORM operations. You should assume that their code is wrong, especially if you see it littered with async_to_sync
.
Async API calls
Here is where the big benefit for async paid off for us. The architecture of ColiVara is to call the GPU service whenever we need to do an AI workload. This is how this looks like:
async with aiohttp.ClientSession() as session:
async with session.post(
EMBEDDINGS_URL, json=payload, headers=headers
) as response:
if response.status != 200:
response_data = await response.json()
In this code, when our application hits the await
keyword, it can yield control back to the event loop, allowing other tasks to run while waiting for the HTTP response.
Here’s what happens:
-
When the code reaches the POST request, it doesn’t block
-
While waiting for the response from
EMBEDDINGS_URL
, the event loop can: -
When the response comes back, our code resumes from where it left off
This is one of the main benefits of async programming – it allows for concurrent execution without using multiple threads or processes, particularly efficient for I/O-bound operations like HTTP requests.
💡
A simple analogy: It’s like ordering food at a restaurant. Instead of standing at the counter waiting for your order (blocking), you sit at your table and can do other things (check email, chat) while waiting for your food to be ready.
Additionally, we can easily process items concurrently which leads to a big performance boost.
async def process_document(document):
return
documents = ['A', 'B', 'C', 'D']
results = await asyncio.gather(
*[process_document(document) for document in documents]
)
Async middleware
One of the main challenges with async Django is middleware. If you use synchronous middleware between an ASGI server and an async view, it switches to sync mode for the middleware and back to async for the view, causing overhead. Designing middleware to work both async and sync is simple; just be mindful of it. Here’s a basic middleware that adds a slash to API requests as an example.
from asgiref.sync import iscoroutinefunction
from django.utils.decorators import sync_and_async_middleware
@sync_and_async_middleware
def add_slash(get_response):
if iscoroutinefunction(get_response):
async def middleware(request):
keep_as_is = any(
x in request.path for x in ["openapi", "swagger", "redoc", "docs"]
)
if not request.path.endswith("https://jonathanadly.com/") and not keep_as_is:
request.path_info = request.path = f"{request.path}/"
return await get_response(request)
else:
def middleware(request):
keep_as_is = any(
x in request.path for x in ["openapi", "swagger", "redoc", "docs"]
)
if not request.path.endswith("https://jonathanadly.com/") and not keep_as_is:
request.path_info = request.path = f"{request.path}/"
return get_response(request)
return middleware
Django will automatically pick the sync vs. the async path depending on the request.
We recommend thoroughly reviewing the middleware you use for async Django to ensure they support async usage. The switch overhead is generally minimal, but it could become problematic if left unchecked.
Conclusion
We believe async Django is ready for production. In theory, there should be no performance loss when using async Django instead of FastAPI for the same tasks. Django’s built-in features greatly simplify and enhance the developer experience. Using async involves some complexity, as the entire code path must be async. In AI workloads, this complexity is often a worthwhile tradeoff.
Keep your files stored safely and securely with the SanDisk 2TB Extreme Portable SSD. With over 69,505 ratings and an impressive 4.6 out of 5 stars, this product has been purchased over 8K+ times in the past month. At only $129.99, this Amazon’s Choice product is a must-have for secure file storage.
Help keep private content private with the included password protection featuring 256-bit AES hardware encryption. Order now for just $129.99 on Amazon!
Support Techcratic
If you find value in Techcratic’s insights and articles, consider supporting us with Bitcoin. Your support helps me, as a solo operator, continue delivering high-quality content while managing all the technical aspects, from server maintenance to blog writing, future updates, and improvements. Support Innovation! Thank you.
Bitcoin Address:
bc1qlszw7elx2qahjwvaryh0tkgg8y68enw30gpvge
Please verify this address before sending funds.
Bitcoin QR Code
Simply scan the QR code below to support Techcratic.
Please read the Privacy and Security Disclaimer on how Techcratic handles your support.
Disclaimer: As an Amazon Associate, Techcratic may earn from qualifying purchases.