Author: Karen
Categories: TECH
Tags: Django Python

Asynchronous APIs are designed to handle many concurrent requests efficiently by avoiding thread blocking I/O operations (such as database queries, network calls, or file access). Instead of assigning one worker per request, async APIs use an event loop to switch between tasks, allowing better resource utilization under high concurrency.

In this article we will compare the performance of a sync django view running with a gunicorn server and an async django view running with an async uvicorn server. In both cases we call a test api and although it is expected for the async view running in a event loop and not being constrained by CPU threads to be significaltly faster, the results show the opposite.

To test the performance, we use hey load testing tool, which can be installed on Linux systems using:

sudo apt install hey

Defining Views

We define two views, sync_view and an async_view, both calling the same external API endpoint.

import requests
from django.http import JsonResponse
import httpx


def sync_view(request):
    r = requests.get("https://jsonplaceholder.typicode.com/todos/1")
    return JsonResponse(r.json())


async def async_view(request):
    async with httpx.AsyncClient() as client:
        r = await client.get("https://jsonplaceholder.typicode.com/todos/1")
    return JsonResponse(r.json())

The routes are:

/sync/test → sync view
/async/test → async view

and the corresponding URL mappings are:

path('async/test', async_view, name="async-test"),
path('sync/test', sync_view, name="sync-test"),

Load Testing Setup

The tests were run inside Docker containers using:

  • Gunicorn for the sync view
  • Uvicorn for the async view

Sync View Testing

To test the sync view performance (sync/test). We first run the Django server with Gunicorn with 4 workers and 10 threads per worker, This means simutanously 40 requests can be served.

gunicorn project.wsgi:application --workers 4 --threads 10 --bind 0.0.0.0:8000

We send 150 requests 30 of them being concurrent, 300 requests with 50 concurrency and 1000 requests with 100 concurency and log the results.

Async View Testing 

To test the async view performance (async/test), we run the Django server with uvicorn. 

uvicorn project.asgi:application --workers 4 --host 0.0.0.0 --port 8000

Terminology

  • Requests Per Second (RPS) measures the number of requests a server is able to handle and complete per second under a given load.

  • Average latency shows how long a request takes on average.

  • P95 latency means that 95% of requests completed faster than a given value (for example, 1.22s).

Results

Testing results are presented in the table bellow. 

Test Mode RPS Avg latency P95 latency
150 / 30 Sync 41.3 0.45s 1.22s
  Async 35.0 0.69s 1.52s
300 / 50 Sync 86.2 0.46s 0.93s
  Async 38.1 0.94s 1.69s
1000 / 100 Sync 81.4 0.92s 1.88s
  Async 42.0 1.75s 3.04s

The results show that sync view consistently outperforms the async view, as the the sync view has higher RPS, lower average latency and better tail latency.

Takeaway

Async view does not automatically imply faster performance, and for single outbound API calls at moderate load, sync views can often be faster, simpler, and more predictable. 

In this case, the sync setup performs well because Gunicorn provides enough threads to efficiently wait for I/O without overwhelming the system.