Skip to content

# Async support

## Intro

Since version 3.1, Django comes with async views support. This allows you run efficient concurrent views that are network and/or IO bound.

    pip install Django>=3.1 django-ninja

Async views work more efficiently when it comes to:

- calling external APIs over the network
- executing/waiting for database queries
- reading/writing from/to disk drives

Django Ninja takes full advantage of async views and makes it very easy to work with them.

## Quick example

### Code

Let's take an example. We have an API operation that does some work (currently just sleeps for provided number of seconds) and returns a word:

    import time

    @api.get("/say-after")
    def say_after(request, delay: int, word: str):
        time.sleep(delay)
        return {"saying": word}

To make this code asynchronous, all you have to do is add the `async` keyword to a function (and use async aware libraries for work processing - in our case we will replace the stdlib `sleep` with `asyncio.sleep`):

    import asyncio

    @api.get("/say-after")
    async def say_after(request, delay: int, word: str):
        await asyncio.sleep(delay)
        return {"saying": word}

### Run

To run this code you need an ASGI server like Uvicorn or Daphne. Let's use Uvicorn for, example:

To install Uvicorn, use:

    pip install uvicorn

Then start the server:

    uvicorn your_project.asgi:application --reload

> Note: replace `your_project` with your project package name
> `--reload` flag used to automatically reload server if you do any changes to the code (do not use on production)

Note

You can run async views with `manage.py runserver`, but it does not work well with some libraries, so at this time (July 2020) it is recommended to use ASGI servers like Uvicorn or Daphne.

### Test

Go to your browser and open http://127.0.0.1:8000/api/say-after?delay=3&word=hello (delay=3) After a 3-second wait you should see the "hello" message.

Now let's flood this operation with 100 parallel requests:

    ab -c 100 -n 100 "http://127.0.0.1:8000/api/say-after?delay=3&word=hello"

which will result in something like this:

    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    1   1.1      1       4
    Processing:  3008 3063  16.2   3069    3082
    Waiting:     3008 3062  15.7   3068    3079
    Total:       3008 3065  16.3   3070    3083

    Percentage of the requests served within a certain time (ms)
      50%   3070
      66%   3072
      75%   3075
      80%   3076
      90%   3081
      95%   3082
      98%   3083
      99%   3083
     100%   3083 (longest request)

Based on the numbers, our service was able to handle each of the 100 concurrent requests with just a little overhead.

To achieve the same concurrency with WSGI and sync operations you would need to spin up about 10 workers with 10 threads each!

## Mixing sync and async operations

Keep in mind that you can use both sync and async operations in your project, and Django Ninja will route it automatically:

    @api.get("/say-sync")
    def say_after_sync(request, delay: int, word: str):
        time.sleep(delay)
        return {"saying": word}

    @api.get("/say-async")
    async def say_after_async(request, delay: int, word: str):
        await asyncio.sleep(delay)
        return {"saying": word}

## Elasticsearch example

Let's take a real world use case. For this example, let's use the latest version of Elasticsearch that now comes with async support:

    pip install elasticsearch>=7.8.0

And now instead of the `Elasticsearch` class, use the `AsyncElasticsearch` class and `await` the results:

    from ninja import NinjaAPI
    from elasticsearch import AsyncElasticsearch

    api = NinjaAPI()

    es = AsyncElasticsearch()

    @api.get("/search")
    async def search(request, q: str):
        resp = await es.search(
            index="documents",
            body={"query": {"query_string": {"query": q}}},
            size=20,
        )
        return resp["hits"]

## Using ORM

Currently, certain key parts of Django are not able to operate safely in an async environment, as they have global state that is not coroutine-aware. These parts of Django are classified as “async-unsafe”, and are protected from execution in an async environment. The ORM is the main example, but there are other parts that are also protected in this way.

Learn more about async safety here in the official Django docs.

So, if you do this:

    @api.get("/blog/{post_id}")
    async def search(request, post_id: int):
        blog = Blog.objects.get(pk=post_id)
        ...

it throws an error. Until the async ORM is implemented, you can use the `sync_to_async()` adapter:

    from asgiref.sync import sync_to_async

    @sync_to_async
    def get_blog(post_id):
        return Blog.objects.get(pk=post_id)

    @api.get("/blog/{post_id}")
    async def search(request, post_id: int):
        blog = await get_blog(post_id)
        ...

or even shorter:

    @api.get("/blog/{post_id}")
    async def search(request, post_id: int):
        blog = await sync_to_async(Blog.objects.get)(pk=post_id)
        ...

There is a common GOTCHA: Django querysets are lazily evaluated (database query happens only when you start iterating), so this will not work:

    all_blogs = await sync_to_async(Blog.objects.all)()
    # it will throw an error later when you try to iterate over all_blogs
    ...

Instead, use evaluation (with `list`):

    all_blogs = await sync_to_async(list)(Blog.objects.all())
    ...

Since Django version 4.1, Django comes with asynchronous versions of ORM operations. These eliminate the need to use `sync_to_async` in most cases. The async operations have the same names as their sync counterparts but are prepended with a. So using the example above, you can rewrite it as:

    @api.get("/blog/{post_id}")
    async def search(request, post_id: int):
        blog = await Blog.objects.aget(pk=post_id)
        ...

When working with querysets, use `async for` paired with list comprehension:

    all_blogs = [blog async for blog in Blog.objects.all()]
    ...

Learn more about the async ORM interface in the official Django docs.