Vues asynchrones Django et ASGI en 2026 : performance et questions d’entretien
Une plongée en profondeur dans les vues asynchrones Django et ASGI en 2026 : leur fonctionnement interne, quel serveur déployer, l’ORM asynchrone et le piège SynchronousOnlyOperation, plus des questions d’entretien.

Les vues asynchrones de Django permettent à un seul worker de traiter des centaines de requêtes concurrentes limitées par les I/O sans dédier un thread à chaque connexion, mais uniquement lorsque le chemin d'exécution reste asynchrone depuis la vue jusqu'à chaque appel réseau. Depuis que Django 3.1 a introduit les vues async def et que Django 4.1 a ajouté l'interface asynchrone de l'ORM, le framework est devenu une plateforme ASGI crédible, et pourtant la plupart des déploiements en production tournent encore sous WSGI et laissent cette concurrence inexploitée. Ce guide approfondi explique le comportement des vues asynchrones sous ASGI, quels serveurs utiliser en 2026, les pièges de l'ORM qui déclenchent SynchronousOnlyOperation, et les questions d'entretien qui distinguent les ingénieurs ayant déjà livré du Django asynchrone de ceux qui n'en ont que lu la théorie.
Les vues asynchrones sont rentables lorsqu'une requête passe l'essentiel de son temps à attendre des I/O qu'elle ne contrôle pas : API HTTP tierces, services en aval lents ou réponses en streaming de longue durée. Pour du travail limité par le CPU ou des requêtes locales rapides vers la base de données, une vue synchrone soutenue par davantage de workers Gunicorn est généralement plus rapide et bien plus simple à raisonner.
Comment fonctionnent les vues asynchrones Django sous ASGI
WSGI est fondamentalement synchrone : chaque requête en cours occupe un thread ou un processus worker du début à la fin, si bien que 50 requêtes lentes exigent 50 workers. ASGI remplace ce modèle par un event loop qui entrelace de nombreuses requêtes sur un seul worker, en suspendant une coroutine chaque fois qu'elle fait un await sur une I/O et en la reprenant à l'arrivée des données. La documentation officielle de Django sur l'asynchrone décrit les deux adaptateurs qui rendent cette cohabitation possible : sous ASGI, Django exécute les vues async def nativement sur la loop et renvoie les vues synchrones dans un threadpool via sync_to_async ; sous WSGI, il enveloppe les vues asynchrones dans async_to_sync et les exécute jusqu'au bout, annulant tout bénéfice de concurrence.
La conséquence pratique est brutale : une vue async def déployée sur un serveur WSGI se comporte comme une vue synchrone en plus lent. La concurrence ne se matérialise que sur un serveur ASGI. Une vue asynchrone minimale qui appelle une API externe ressemble presque trait pour trait à son équivalent synchrone, la différence se concentrant dans le await.
# views.py
import httpx
from django.http import JsonResponse
async def dashboard(request):
# Sous ASGI, cette coroutine s'exécute sur l'event loop : le worker reste
# donc libre de servir d'autres requêtes pendant que httpx attend l'aller-retour réseau.
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get("https://api.example.com/metrics")
return JsonResponse(response.json())Le gain ne réside pas dans le fait que la requête isolée se termine plus vite ; elle prend le même temps réel. Le gain, c'est que le worker n'est pas bloqué pendant le await, de sorte que le débit sous charge concurrente augmente pendant que le nombre de processus reste constant.
Ce même modèle débloque des schémas de réponse malaisés sous WSGI. Depuis Django 4.2, StreamingHttpResponse accepte un générateur asynchrone, ce qui permet à une vue d'émettre des chunks au fur et à mesure qu'une source amont les produit ; c'est adapté aux Server-Sent Events et aux flux de tokens de LLM relayés, où la connexion reste ouverte pendant plusieurs secondes. Les protocoles bidirectionnels persistants comme les WebSockets passent toujours par Django Channels plutôt que par de simples vues HTTP, mais les deux reposent sur la même fondation ASGI : un projet qui adopte les vues asynchrones a donc déjà payé le coût d'infrastructure des fonctionnalités temps réel à venir.
Choisir un serveur ASGI Django en 2026
Django fournit un objet application ASGI dans asgi.py, mais celui-ci ne s'exécute pas tout seul ; un serveur ASGI dédié pilote l'event loop. La spécification ASGI définit l'interface qu'implémente chacun de ces serveurs, ce qui explique pourquoi ils sont interchangeables au niveau du protocole et diffèrent surtout par la performance et la couverture des protocoles.
| Serveur | Implémenté en | Cas d'usage idéal | |--------|----------------|----------| | Uvicorn | Python (uvloop) | ASGI généraliste, HTTP/1.1, choix par défaut courant | | Daphne | Python | Django Channels, WebSockets, HTTP/2 | | Granian | Rust | Débit brut maximal, HTTP/1 et HTTP/2 | | Hypercorn | Python | HTTP/2 et HTTP/3 (QUIC) |
Pour la plupart des déploiements en 2026, Uvicorn lancé comme classe de worker Gunicorn reste le choix fiable : Gunicorn supervise le cycle de vie des processus, redémarre les workers plantés et gère les signaux, tandis qu'Uvicorn pilote l'event loop à l'intérieur de chacun. Un détail de migration fait trébucher les mises à jour : depuis Uvicorn 0.30, les classes de worker Gunicorn ont quitté le cœur d'Uvicorn pour un paquet distinct uvicorn-worker, ce qui a modifié le chemin d'import.
# Installer d'abord le paquet worker : pip install uvicorn-worker
gunicorn myproject.asgi:application \
--workers 4 \
--worker-class uvicorn_worker.UvicornWorker \
--bind 0.0.0.0:8000Les équipes en quête de débit maximal se tournent de plus en plus vers Granian, un serveur écrit en Rust qui élimine le coût du parsing HTTP côté Python. Quel que soit le choix, c'est la question du déploiement qui fait que l'asynchrone n'apparaît en production que lorsqu'il est configuré délibérément ; la mise en place propre à ASGI reflète le soin opérationnel abordé dans le module questions d'entretien sur le déploiement de Django.
L'ORM asynchrone et le piège SynchronousOnlyOperation
Django 4.1 a ajouté des méthodes de requête asynchrones à travers tout l'ORM : aget, acreate, asave, adelete, acount, aexists, aaggregate, ainsi que l'itération avec async for. Elles offrent une surface compatible avec l'asynchrone, mais une réserve critique subsiste en 2026 : la couche base de données sous-jacente n'est pas nativement asynchrone. Même avec psycopg3, Django exécute les requêtes dans un threadpool et les expose via des wrappers asynchrones ; l'ORM asynchrone améliore donc l'ergonomie et évite de bloquer la loop, sans pour autant offrir de véritables I/O base de données asynchrones de bout en bout.
Le mode de défaillance le plus fréquent consiste à appeler une méthode synchrone de l'ORM directement à l'intérieur d'une vue asynchrone. Django détecte le contexte asynchrone et refuse, en levant SynchronousOnlyOperation pour empêcher qu'un appel bloquant ne fige l'event loop pour toutes les autres requêtes partageant ce worker.
Cette exception est un garde-fou, pas un bug. Recourir à DJANGO_ALLOW_ASYNC_UNSAFE=true pour la faire taire réintroduit exactement le comportement bloquant que l'asynchrone était censé éliminer. Le correctif approprié est une méthode asynchrone de l'ORM (aget au lieu de get) ou un wrapper sync_to_async explicite autour du code qui n'a pas d'équivalent asynchrone.
Les vues asynchrones se lisent proprement dès que les méthodes de requête asynchrones remplacent leurs jumelles synchrones. Le motif ci-dessous compte les lignes et parcourt les dix premiers enregistrements sans jamais quitter l'event loop.
# views.py
from django.http import JsonResponse
from .models import Article
async def latest_articles(request):
# acount() et l'itération asynchrone sont des wrappers de l'ORM sûrs en asynchrone.
count = await Article.objects.acount()
titles = []
async for article in Article.objects.order_by("-created_at")[:10]:
titles.append(article.title)
return JsonResponse({"count": count, "titles": titles})Certains codes n'ont pas d'équivalent asynchrone : un SDK tiers qui ne propose que des appels synchrones, ou une chaîne dense de traversées d'ORM qu'il ne vaut pas la peine de réécrire. L'envelopper dans sync_to_async de la bibliothèque asgiref délègue le travail à un thread et garde la loop réactive. L'argument thread_sensitive=True est le choix sûr par défaut, car il achemine chaque appel enveloppé vers un unique thread d'exécution partagé, préservant l'état de connexion et de transaction de la base de données, qui se briserait si les appels se dispersaient sur des threads arbitraires.
# views.py
from asgiref.sync import sync_to_async
from .services import build_report # une fonction synchrone
async def report_view(request):
# thread_sensitive=True maintient les appels enveloppés sur un unique thread
# partagé, préservant l'état de connexion et de transaction à la frontière.
report = await sync_to_async(build_report, thread_sensitive=True)(request.user.id)
return JsonResponse(report)Performance de Django en asynchrone : quand la concurrence l'emporte
La performance de Django en asynchrone est une affaire de chevauchement d'I/O, pas de vitesse brute. Le gain le plus net vient de la parallélisation d'appels réseau indépendants avec asyncio.gather : trois requêtes externes de 200 ms chacune coûtent environ 600 ms en séquence, mais se terminent en 200 ms environ en concurrence, parce que les await se chevauchent sur la même loop.
# views.py
import asyncio
import httpx
from django.http import JsonResponse
async def aggregate(request):
async with httpx.AsyncClient(timeout=5.0) as client:
# Trois appels indépendants s'exécutent en concurrence au lieu de l'un après l'autre.
weather, prices, news = await asyncio.gather(
client.get("https://api.example.com/weather"),
client.get("https://api.example.com/prices"),
client.get("https://api.example.com/news"),
)
return JsonResponse({
"weather": weather.json(),
"prices": prices.json(),
"news": news.json(),
})Le cas inverse compte tout autant. Une vue qui exécute une seule requête locale rapide ne gagne rien à l'asynchrone et y perd souvent, car chaque requête paie désormais l'ordonnancement de l'event loop plus un saut vers un threadpool jusqu'au driver synchrone de la base de données. L'asynchrone impose aussi une taxe plus subtile à chaque frontière sync/async : mélanger des middlewares synchrones et asynchrones oblige Django à basculer entre la loop et un thread à chaque transition, et une requête qui franchit cette limite plusieurs fois par réponse accumule une latence réelle. Maintenir la pile de middleware uniformément asynchrone, ou uniformément synchrone, supprime ces bascules.
La décision se ramène à la nature de la charge de travail plutôt qu'à une préférence pour une syntaxe moderne.
| Charge de travail | Meilleur choix par défaut |
|----------|----------------|
| Plusieurs appels lents à des API externes par requête | Vue asynchrone avec asyncio.gather |
| Streaming de longue durée ou Server-Sent Events | Vue asynchrone avec un générateur asynchrone |
| Requête unique et rapide en base de données, peu de CPU | Vue synchrone, plus de workers Gunicorn |
| Rendu ou calcul limité par le CPU | Vue synchrone, déléguer à une file de tâches |
Pour les traitements réellement longs, les vues asynchrones sont carrément le mauvais outil ; maintenir une requête ouverte pour effectuer plusieurs minutes de travail gaspille la connexion quel que soit le modèle de concurrence. Ce travail relève d'un worker d'arrière-plan, un compromis exploré dans le guide sur les tâches asynchrones Celery dans Django.
L'adoption devrait suivre la mesure plutôt que l'intuition. La manière honnête de justifier l'asynchrone consiste à réaliser un test de charge de l'endpoint précis avec un outil comme Locust ou k6 sous une concurrence réaliste, en comparant une version synchrone sur N workers à une version asynchrone sur le même matériel. Les endpoints dominés par un unique appel externe à forte latence affichent un écart net en faveur de l'asynchrone ; les endpoints dominés par des requêtes rapides montrent souvent l'asynchrone légèrement en retrait une fois le saut vers le threadpool comptabilisé. Profiler d'abord la requête révèle aussi si le goulot d'étranglement est bien lié aux I/O, car l'asynchrone ne fait rien pour une vue lente à cause de requêtes non indexées, un problème traité dans le tutoriel sur l'optimisation des requêtes de l'ORM Django.
Prêt à réussir tes entretiens Django ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Questions d'entretien Django asynchrone pour 2026
Un entretien sur Django asynchrone cherche à savoir si un candidat comprend le modèle ou se contente d'en reconnaître les mots-clés. Les questions ci-dessous reflètent ce que les entretiens seniors demandent réellement en 2026, chacune accompagnée de la réponse que donnerait un ingénieur expérimenté.
Qu'est-ce qui change lorsqu'une vue asynchrone s'exécute sous WSGI plutôt que sous ASGI ? Sous WSGI, Django enveloppe la coroutine dans async_to_sync et l'exécute jusqu'au bout sur le thread de la requête ; elle se comporte donc comme une vue synchrone en plus lent, sans aucun bénéfice de concurrence. Seul un serveur ASGI faisant tourner un event loop permet au worker de servir d'autres requêtes pendant un await.
Pourquoi Article.objects.get(pk=1) lève-t-il SynchronousOnlyOperation à l'intérieur d'une vue asynchrone ? L'appel synchrone de l'ORM bloquerait l'event loop et paralyserait toutes les autres requêtes de ce worker. Django détecte le contexte asynchrone et refuse. Le correctif est la méthode asynchrone await Article.objects.aget(pk=1), ou sync_to_async pour du code sans équivalent asynchrone.
L'ORM asynchrone de Django est-il réellement non bloquant ? Non. Les méthodes asynchrones sont des wrappers ergonomiques ; les requêtes sous-jacentes s'exécutent toujours dans un threadpool, parce que les drivers de base de données ne sont pas intégrés pour une exécution asynchrone native de bout en bout. Le bénéfice est de garder la loop libre, pas d'obtenir des I/O base de données parallèles au niveau du driver.
Quand faut-il choisir les vues asynchrones plutôt qu'une file de tâches comme Celery ? Les vues asynchrones conviennent à la concurrence d'I/O à l'échelle de la requête qui doit se terminer avant la réponse, comme l'agrégation de plusieurs API. Le travail qui survit à la requête, nécessite des retries ou tourne pendant plusieurs minutes relève de Celery ou des tâches d'arrière-plan de Django, car maintenir une connexion HTTP ouverte pour cela gaspille les ressources serveur.
Quel est le coût du mélange de middlewares synchrones et asynchrones ? Chaque transition entre un composant synchrone et un composant asynchrone oblige Django à sauter entre l'event loop et un thread via sync_to_async ou async_to_sync. Une requête qui franchit cette frontière à répétition paie un surcoût cumulatif ; une pile de middleware uniforme est donc plus performante qu'une pile entrelacée.
Conclusion
- Les vues asynchrones n'apportent de la concurrence que sur un serveur ASGI ; la même vue sous WSGI s'exécute de façon synchrone via
async_to_sync. - Déployer Uvicorn via le paquet
uvicorn-workersous Gunicorn, ou Granian pour un débit maximal, et traiter la mise en place d'ASGI comme une étape de déploiement explicite. - Recourir aux vues asynchrones quand une requête attend plusieurs appels d'I/O externes lents ; garder synchrones les endpoints à requêtes rapides et limités par le CPU, avec davantage de workers.
- Utiliser
aget,acreateetasync fordans les vues asynchrones, en gardant à l'esprit que l'ORM exécute toujours les requêtes dans un threadpool plutôt que de manière nativement asynchrone. - Traiter
SynchronousOnlyOperationcomme un garde-fou : le corriger avec des méthodes asynchrones ousync_to_async(thread_sensitive=True), jamais en désactivant la vérification. - Maintenir la pile de middleware uniformément synchrone ou asynchrone pour éviter de payer une pénalité de changement de thread à chaque franchissement de frontière.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Django et Celery : traitement asynchrone des tâches et questions d'entretien 2026
Guide Django Celery avec exemples pratiques, routage de tâches, planification Beat, configuration production et questions d'entretien technique 2026.

Django ORM : Optimiser vos requêtes pour des performances maximales
Guide complet pour optimiser les requêtes Django ORM. select_related, prefetch_related, index, analyse N+1 et techniques avancées pour des applications performantes.

Django et PostgreSQL en 2026 : indexation, recherche plein texte et questions d'entretien
Guide pratique de l'optimisation Django PostgreSQL : index B-tree, partiels et couvrants, recherche plein texte avec SearchVector et GIN, plus les questions d'entretien 2026.