# 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. - Published: 2026-07-03 - Updated: 2026-07-07 - Author: SharpSkill - Tags: django, async, asgi, python, performance - Reading time: 10 min --- 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. > **Quand l'asynchrone aide vraiment** > > 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](https://docs.djangoproject.com/en/5.2/topics/async/) 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`. ```python # 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](https://asgi.readthedocs.io/en/latest/) 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](https://www.uvicorn.org/) 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. ```bash # 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:8000 ``` Les é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](/technologies/django/interview-questions/django-deployment). ## 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. > **SynchronousOnlyOperation est une fonctionnalité** > > 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. ```python # 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](https://github.com/django/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. ```python # 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. ```python # 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](/blog/django/django-celery-async-tasks-interview-2026). 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](/blog/django/django-orm-optimizing-queries). ## 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-worker` sous 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`, `acreate` et `async for` dans 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 `SynchronousOnlyOperation` comme un garde-fou : le corriger avec des méthodes asynchrones ou `sync_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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/django/django-async-views-asgi-2026