Asynchroniczność w Django – zbawienie czy pułapka?
Django długo był synonimem synchronizmu – każdy request obrabiany w osobnym wątku lub procesie, krok po kroku. Jednak świat webu się zmienia: real-time, WebSockety, integracje z masą zewnętrznych API. Frameworki jak Node.js czy FastAPI chwalą się asynchronicznością, więc i Django nie chce zostać w tyle. Od wersji 3.1 Django wprowadziło wsparcie dla kodu asynchronicznego. Tylko czy to naprawdę przyspieszy nasze aplikacje? Przyjrzyjmy się, co Django oferuje asynchronicznie, kiedy ma to sens, a gdzie czekają pułapki. Będzie konkretnie, zaczepnie i z przykładami – porównamy kod synchroniczny vs. asynchroniczny, zahaczymy o wydajność i dobre praktyki.
Wbudowane mechanizmy asynchroniczne w Django
Django od wersji 3.1 pozwala pisać widoki jako asynchroniczne korutyny (async def
) oraz uruchomić całą aplikację w trybie ASGI (Asynchronous Server Gateway Interface) zamiast tradycyjnego WSGI (Asynchronous support | Django documentation | Django). Oznacza to, że znaczna część „stacku” obsługi requestów może działać w pełni asynchronicznie – od widoków, przez middleware, aż po wykonywanie zapytań do bazy (choć tu są pewne ograniczenia, o czym za moment). Poniżej przegląd wbudowanych mechanizmów async, jakie oferuje Django:
Asynchroniczne widoki (async views)
Najważniejsza nowość to asynchroniczne widoki. Każdy widok funkcji czy metoda w klasowym CBV może zostać zadeklarowany jako async def
, dzięki czemu Django wykryje go jako korutynę i będzie w stanie go awaitować (Asynchronous support | Django documentation | Django). Przykład: zamiast zwykłego def my_view(request): ...
piszemy async def my_view(request): ...
. Wewnątrz takiego widoku możemy swobodnie używać await
do nieblokujących operacji I/O – np. wywoływać asynchroniczne zapytania HTTP do zewnętrznych API, odczytywać pliki, czekać na dane itp. To rozwiązanie działa nawet pod zwykłym WSGI, ale wtedy Django i tak uruchomi wewnętrznie pętlę zdarzeń dla pojedynczego requestu – zadziała, lecz bez korzyści wydajnościowych i z narzutem (Asynchronous support | Django documentation | Django) (Asynchronous support | Django documentation | Django). Pełnię możliwości async widoki pokazują dopiero po wdrożeniu aplikacji na serwer ASGI (np. Uvicorn, Daphne), gdzie wiele requestów może współbieżnie korzystać z jednej pętli zdarzeń bez potrzeby tworzenia nowych wątków dla każdej odpowiedzi.
Co potrafią async widoki? Pozwalają obsłużyć jednocześnie setki czy tysiące połączeń, o ile logika jest głównie I/O-bound (czeka na zewnętrzne zasoby) zamiast CPU-bound. Z dokumentacji Django: „Główną zaletą jest możliwość obsłużenia setek połączeń bez użycia wielu wątków Pythona” (Asynchronous support | Django documentation | Django). To otwiera drogę do implementacji np. długotrwałych streamów, long-pollingu, SSE czy innych mechanizmów oczekujących na zdarzenia, bez blokowania workerów.

Asynchroniczne zapytania do bazy danych (ORM async)
A co z bazą danych? Przecież klasyczny ORM Django to kod synchroniczny, który blokuje wątek w trakcie zapytania SQL. Tu zaszły duże zmiany na plus. W najnowszych Django wiele operacji na QuerySet ma już swoje asynchroniczne odpowiedniki. Każda metoda, która wykonuje zapytanie do DB i zwraca wyniki, posiada wersję z prefiksem a
(od „async”) (Making queries | Django documentation | Django). Przykładowo:
get()
ma odpowiednikaget()
– aby pobrać obiekt nie blokując pętli zdarzeń (Making queries | Django documentation | Django),create()
->acreate()
,first()
/last()
->afirst()
/alast()
,count()
->acount()
,- nawet kasowanie
delete()
ma async wariantadelete()
.
Co więcej, asynchroniczna iteracja po QuerySet jest wspierana – można użyć async for
do pobierania kolejnych wyników zapytania w sposób nieblokujący (Making queries | Django documentation | Django). Przykładowo:
# Async iteracja po QuerySet
async for book in Book.objects.filter(author="Alice"):
print(book.title)
Behind the scenes Django zadba, by takie iterowanie wykonywało zapytania partiami asynchronicznie.
Brzmi świetnie, ale warto pamiętać, że nie wszystko w ORM jest już async. Operacje transakcyjne (np. użycie transaction.atomic()
albo kilku zapytań, które muszą być w jednej transakcji) nie są obsługiwane w trybie asynchronicznym – próba użycia transakcji w async skończy się wyjątkiem SynchronousOnlyOperation
(Asynchronous support | Django documentation | Django) (Making queries | Django documentation | Django). Django po prostu nie pozwoli uruchomić pewnych rzeczy z ORM w pętli zdarzeń, bo nie są one jeszcze bezpieczne w async. Planowane jest pełne wsparcie ORM dla asynchroniczności w przyszłych wersjach, ale na razie "we’re still working on async support for the ORM" i w razie potrzeby trzeba się ratować narzędziami typu sync_to_async
(Asynchronous support | Django documentation | Django) (o czym później). Na szczęście większość prostych operacji CRUD już działa asynchronicznie.
Asynchroniczne middleware
Middleware w Django to zwykle proste funkcje/klasy opakowujące requesty i response (np. do uwierzytelniania, sesji, CSRF itp.). Od momentu wejścia async, middleware również mogą być async. W praktyce oznacza to, że możemy napisać własny middleware jako korutynę async def __call__
albo async def process_view
itd., a Django wykryje to i również wykona w pętli zdarzeń. Co istotne, Django potrafi mieszać middleware sync i async w jednym łańcuchu obsługi requestu – ale nie odbywa się to magicznie bez kosztów. Gdy w aplikacji pod ASGI mamy choć jeden synchronczny middleware, Django musi przełączyć się z trybu async na sync, uruchamiając ten fragment w wątku, a potem z powrotem na async dla kolejnych elementów (Asynchronous support | Django documentation | Django). Każde takie przełączenie kontekstu to ~1ms narzutu (Asynchronous support | Django documentation | Django). Jeśli więc np. między ASGI serwerem a naszym async widokiem wisi kilka domyślnych middleware napisanych tradycyjnie, to Django i tak będzie zmuszone oddelegować je do wątków, co może zniwelować zyski wydajności async (Asynchronous support | Django documentation | Django). Dobra wiadomość jest taka, że część wbudowanych middleware Django ma już wsparcie async, a własne też możemy tak zaimplementować. Przy debugowaniu można nawet włączyć logowanie komunikatów o adaptacji middleware (logger django.request
– wtedy zobaczymy np. info “Asynchronous handler adapted for middleware X...” (Asynchronous support | Django documentation | Django)).
W skrócie: Django obsłuży asynchroniczne middleware, ale mieszanie trybów jest wrażliwe na wydajność. Idealnie, w całym łańcuchu obsługi requestu pod ASGI powinny być albo wyłącznie komponenty async, albo tak zminimalizowane przełączenia, jak to możliwe.
WebSockety i długotrwałe połączenia (ASGI, Django Channels)
Wbudowany mechanizm Django do obsługi protokołu HTTP (zarówno WSGI jak i ASGI) dotyczy tradycyjnych żądań request/response. WebSockety to zupełnie inny tryb komunikacji (połączenie dwukierunkowe, utrzymywane przez dłuższy czas). Czy Django potrafi je obsługiwać? Tak – dzięki trybowi ASGI mamy taką możliwość, choć nie jest to tak proste jak z widokami HTTP. Sam ASGI to specyfikacja, która pozwala Pythonowym frameworkom obsługiwać nie tylko HTTP, ale i dowolne protokoły asynchroniczne, właśnie takie jak WebSocket czy Server-Sent Events (How to Add Websockets to a Django App without Extra Dependencies). Django 3.0+ udostępnia podstawowy serwer ASGI (django.core.asgi.get_asgi_application()
), który obsługuje HTTP. Aby dodać WebSockety, musimy rozszerzyć domyślną aplikację ASGI o własny handler WebSocketów albo sięgnąć po Django Channels.
Django Channels to oficjalny projekt rozszerzający Django poza HTTP – dodaje warstwę obsługi protokołów takich jak WebSocket (a także np. protokoły IoT) w sposób zintegrowany z Django (Django Channels — Channels 4.2.0 documentation). Channels wprowadza koncepcję Consumers (konsumentów zdarzeń), które mogą być synchroniczne lub asynchroniczne, a framework zadba o całą otoczkę (routing komunikatów, utrzymanie połączeń, integracja z sesjami Django itp.). W dużym skrócie: jeśli planujemy czat na żywo, powiadomienia w czasie rzeczywistym, streaming danych do klienta – Channels jest naszym przyjacielem, bo dostajemy bogate API do WebSocketów niemal „out of the box”.
Możliwe jest też podejście bardziej low-level: napisanie własnej obsługi WebSocketów bezpośrednio w ASGI bez dodatkowych zależności. W dokumentacji ASGI pokazuje się wzorzec, gdzie w pliku asgi.py
opakowujemy główną aplikację Django dodatkową async-funkcją rozróżniającą scope['type']
: jeśli to "http"
– kierujemy do Django, jeśli "websocket"
– obsługujemy w kodzie własnym (Websockets in Django (without Channels) - DEV Community). To działa, ale jest bardziej złożone i łatwo się pomylić. Dlatego w praktyce Channels są zalecaną drogą do WebSocketów w Django – choć formalnie to zewnętrzna biblioteka, to jednak ”oficjalna” część ekosystemu Django.
Podsumowując, wbudowane wsparcie Django dla asynchroniczności obejmuje widoki, middleware, częściowo ORM oraz infrastrukturę ASGI pozwalającą obsługiwać nowe typy połączeń. Teraz kluczowe pytanie – po co to wszystko?
Korzyści z asynchroniczności – kiedy naprawdę się opłaca?
Asynchroniczność bywa obecnie słowem-wytrychem obiecującym lepszą wydajność. Jednak trzeźwe spojrzenie każe zapytać: jakie realne korzyści przyniesie to mojej aplikacji? Oto najważniejsze zalety w modelu async i sytuacje, gdy warto po nie sięgnąć:
- Więcej jednoczesnych połączeń bez „drogi” – W modelu synchronicznym, jeśli jeden request czeka 2 sekundy na odpowiedź z zewnętrznego API, to wątek obsługujący ten request jest przez te 2 sekundy zajęty (nic innego nie zrobi). Przy dużym ruchu musimy mieć wiele workerów, żeby obsłużyć równolegle dziesiątki takich czekających żądań – a to koszt pamięci i narzut przełączania kontekstu między wątkami/procesami. W modelu asynchronicznym jeden worker (jeden wątek z pętlą zdarzeń) może obsłużyć wiele oczekujących zadań naraz, bo gdy jedno czeka na I/O, pętla przełącza się do obsługi innego. To ogromna poprawa skalowalności – możemy utrzymać mnóstwo otwartych połączeń bez proporcjonalnego zwiększania zasobów (Optimizing Django Application Performance: Async vs. Sync Views | by Ewho Ruth | Towards Dev). Typowy przypadek to aplikacje I/O-bound: np. serwer proxy agregujący dane z kilku API, usługi integrujące się z zewnętrznymi serwisami (płatności, mapy, pogoda itp.) czy serwujące pliki/streaming mediów. Async pozwoli obsłużyć więcej takich zapytań jednocześnie na tej samej infrastrukturze.
- Responsywność i krótszy czas odpowiedzi przy I/O – Nawet pojedynczy request może skorzystać na async, jeśli musi np. wykonać kilka niezależnych operacji I/O. Przykład: widok ma pobrać dane z dwóch różnych API i złożyć je w jedną odpowiedź. W podejściu synchronicznym najpierw czekamy na API A, potem na API B – sumaryczny czas to A+B. W asynchronicznym widoku możemy wysłać oba requesty równolegle (np. korzystając z biblioteki HTTPX) i awaitować je jednocześnie, dzięki czemu łączny czas to około
max(A, B)
zamiastA+B
. Użytkownik dostanie odpowiedź szybciej. Async widoki nie blokują głównej pętli, więc strona pozostaje responsywna – inne zapytania nie muszą czekać aż skończymy te zewnętrzne operacje (Optimizing Django Application Performance: Async vs. Sync Views | by Ewho Ruth | Towards Dev). To szczególnie ważne w aplikacjach gdzie zdarzają się okresowo wolniejsze zapytania – async sprawi, że „wolny” request nie spowolni pozostałych tak jak w trybie sync. - Real-time features – Jak wspomnieliśmy, rzeczy typu WebSockety, SSE, long-polling są praktycznie nie do zrealizowania efektywnie w czysto synchronicznym Django (choćby przez ograniczenia WSGI). Dzięki async i ASGI możemy zaoferować użytkownikom prawdziwy real-time: powiadomienia push, aktualizacje na żywo, czaty. I to bez konieczności stawiania osobnych serwerów w innym języku – wszystko w Django. Long-polling czy streaming odpowiedzi (wypychanie fragmentów danych co pewien czas) już nie zablokuje nam całego workera – pętla asynchroniczna obsłuży to elegancko. Dla aplikacji wymagających interaktywności (gry przeglądarkowe, dashboardy finansowe na żywo, aplikacje IoT) to game-changer.
- Lepsze wykorzystanie zasobów serwera – Async może w niektórych scenariuszach poprawić wydajność nawet czysto synchronicznej aplikacji hostowanej pod ASGI, przez to że sama wewnętrzna obsługa requestu jest nieco bardziej efektywna. Django dokumentacja wspomina, że czasem nawet bez pisania async kodu, przejście na ASGI z dobrym serwerem może minimalnie polepszyć throughput (Asynchronous support | Django documentation | Django) (choć to subtelne i zależne od przypadku). Ogólnie, asynchroniczność pozwala uwolnić wątek gdy tylko czekamy, więc procesy mogą zużywać CPU na rzeczywistą pracę, a nie stanie bezczynnie. W skali dużej aplikacji I/O-bound to oznacza mniejszy narzut na context-switching i potencjalnie niższe zużycie pamięci (mniej równoczesnych wątków/procesów potrzebnych by obsłużyć ten sam ruch).
Czy to znaczy, że zawsze i wszędzie powinniśmy przerabiać kod na async? Nie! O tym w kolejnym punkcie – bo diabeł tkwi w szczegółach.

Wyzwania i pułapki asynchroniczności w Django
Przejście na model asynchroniczny to nie tylko zmiana słówka def
na async def
. Czekają nas nowe klasy problemów. Oto najważniejsze wyzwania i pułapki, na które trzeba uważać:
- ORM i inne sync-only części Django – Jak już wspomniano, nie cały Django “nadąża” za async. Głównym problemem jest ORM, który historycznie jest synchroniczny. Django celowo blokuje wywołania ORM z pętli event loop, jeśli dana operacja nie ma async odpowiednika, rzucając wyjątek
SynchronousOnlyOperation
(Making queries | Django documentation | Django). Na przykład, wywołanieUser.objects.get()
wewnątrz async widoku od razu zakończy się błędem z taką informacją. Trzeba wtedy sięgnąć po obejście: użyć metodaget()
, albo jeśli ich nie ma – uruchomić kod synchroniczny w oddzielnym wątku za pomocąasgiref.sync.sync_to_async
. Podobnie inne kawałki Django, które nie są thread-safe lub zależą od globalnego stanu, zostały oznaczone jako “async-unsafe” (niebezpieczne dla async) i również spowodują wyjątek ochronny. Np. korzystanie z obiektu HttpRequest (request) poza bieżącą pętlą itp. W skrócie: musimy uważać, które API Django można wprost awaitować, a które absolutnie nie. Lista ciągle się kurczy z każdą wersją (bo coraz więcej jest wspierane natywnie), ale w 2025 r. wciąż natkniemy się na brakujące elementy – szczególnie w mniej typowych obszarach (transakcje, specyficzne metody QuerySet, niektóre biblioteki Django). - GIL – brak przyspieszenia dla kodu CPU-bound – Asynchroniczność w Pythonie nie oznacza równoległego wykonywania kodu na wielu wątkach jednocześnie. CPython ma słynny Global Interpreter Lock (GIL), który sprawia, że w danym momencie wykonywany jest kod tylko jednego wątku. Asyncio (podobnie jak wątki) tego nie omija – jeśli nasz kod potrzebuje wykonać ciężkie obliczenia, np. przetworzyć duży plik w pamięci czy zaszyfrować hasło, to dopóki nie “puści” on kontroli (nie zrobi
await
na coś), żadna inna korutyna nie ruszy (What are the advantages of asyncio over threads? - Page 2 - Ideas - Discussions on Python.org). Innymi słowy, async pomaga tylko gdy mamy co jakiś czas wolne przebiegi (awaity) na operacje I/O. Jeśli nasz request jest głównie intensywnym liczeniem na CPU (zadanie CPU-bound), to w trybie async będzie on tak samo blokował jak w sync – a nawet gorzej, bo cała pętla zdarzeń stanie w miejscu! To duża pułapka: nie ma sensu przenosić do async algorytmów matematycznych, kompresji, itp. – tutaj lepszy będzie tradycyjny multithreading (choć w Pythonie przez GIL ograniczony) albo wręcz multiprocessing (równoległość na poziomie procesów). Nawet dokumentacja zaznacza, że async views są najefektywniejsze dla operacji I/O-bound, a dla CPU-bound korzyść jest znikoma (Unlocking Performance: A Guide to Async Support in Django - DEV Community). Podsumowanie: GIL wciąż obowiązuje, async go nie znosi. Jedna pętla asyncio na jednym wątku w danej chwili i tak wykonuje tylko jedną rzecz naraz, więc wydajnościowo przy czystym CPU nic nie zyskujemy. - Kompatybilność middleware i „mieszany” stack – Omówiony wyżej problem mieszania sync/async dotyczy nie tylko middleware, ale ogólnie całej ścieżki obsługi requestu. Jeśli nasz widok jest async, a gdzieś po drodze do niego jest kawałek kodu sync (może nawet jakiś starter wczytujący aplikację lub routing), Django zrobi tzw. „adaptację” – użyje wątku pomocniczego, by wykonać ten fragment, po czym wróci do async. Deweloper nie musi o tym pisać kodu – to dzieje się pod maską – ale konsekwencją jest dodatkowy narzut. Każdy punkt przełączenia trybu kosztuje ok. 1ms (Asynchronous support | Django documentation | Django). Niby niedużo, ale jeśli takich punktów w jednym żądaniu jest kilka (np. 3 middleware sync przed widokiem async), to dodajemy łatwo 3-5ms, co może stanowić np. 30% czasu obsługi prostego requestu! W skrajnych przypadkach, jak zauważyli użytkownicy, czysto synchroniczny widok pod ASGI może mieć znacznie większe opóźnienie bazowe niż pod WSGI (raportowano np. ~1ms vs ~15ms w pewnym teście) właśnie z powodu kosztu przełączania i zarządzania wątkami (Huge performance difference when using ASGI and WSGI - Async - Django Forum) (Huge performance difference when using ASGI and WSGI - Async - Django Forum). Dlatego tak ważne jest, by uważać na mieszanie – najlepiej migrację na async zacząć od usunięcia/uproścenia tych elementów, które nie są jeszcze async, żeby nie psuć sobie zysków wydajności. Jeśli używamy dużo własnych middleware lub bibliotek wpinających się w request/response, trzeba sprawdzić ich zgodność z async.
- Biblioteki firm trzecich – Ekosystem Django jest ogromny i wiele dodatków (Django REST Framework, biblioteki autoryzacji, upload plików itd.) pisano przed erą async. Wiele z nich nadal zakłada model synchroniczny. Może się okazać, że np. jakiś custom validator albo storage backend do plików wykonuje operacje sieciowe/dyskowe w sposób blokujący, a my wywołujemy go w async widoku – to zablokuje pętlę. Narzędzia jak DRF dodały co prawda wstępne wsparcie (DRF 3.12+ obsłuży async views), ale wciąż trafimy na np. bibliotekę do płatności, która nie ma async API. Trzeba wówczas kombinować: albo znaleźć alternatywę, albo znów oddelegować wywołanie do wątku (co odbiera trochę uroku asynchroniczności). Ten okres przejściowy, gdzie pół świata jest async, a pół nie, to pewne pole minowe. Dlatego przed migracją warto zinwentaryzować nasze zależności Pythona pod kątem czy są async-friendly.
- Zwiększona złożoność i trudniejsze debugowanie – Kod asynchroniczny bywa trudniejszy do zrozumienia i testowania. Pojawiają się nowe rodzaje błędów: race condition w logice współbieżnej, anulowanie zadań gdy użytkownik przerwie połączenie (np.
asyncio.CancelledError
gdy klient się rozłączy w trakcie long-pollingu (Asynchronous support | Django documentation | Django)), deadlocki jak zapomnimy oawait
. Stos błędów (traceback) przy async bywa mniej czytelny – stack przerywany jest naawait
i kontynuowany gdzie indziej. Trzeba też uważać na współdzielenie zmiennych globalnych – w trybie sync zwykle jeden request = jeden wątek, więc proste rzeczy jak użycie modułu lokalnego dla wątku mają sens. W async ten sam wątek obsługuje wiele requestów na przemian, więc np. globalny obiekt cache w pamięci używany bez blokad może nieoczekiwanie być używany współbieżnie przez różne requesty. Ogółem, async dodaje pewien narzut mentalny dla programisty – trzeba opanować nowe idiomy (pętle zdarzeń, futures, tasks, itp.), co dla osób przyzwyczajonych do liniowego przepływu Django może być wyzwaniem.
Mimo tych pułapek, async w Django jest wykonalne i bezpieczne – po prostu wymaga świadomości. Jak zatem prawidłowo migrować/stosować async? Poniżej kilka technik i dobrych praktyk.
Jak poprawnie stosować asynchroniczność w Django – techniki i dobre praktyki
Jeśli zdecydowaliśmy, że asynchroniczność ma sens w naszym przypadku, warto robić to z głową. Oto praktyczne wskazówki i techniki, które pomogą wycisnąć z async w Django to, co najlepsze, unikając wpadek:
- Uruchom aplikację na serwerze ASGI – Brzmi banalnie, ale to podstawa. W środowisku deweloperskim django uruchamia własny serwer (który od wersji 3.0 jest ASGI, więc OK). Jednak w produkcji popularne gunicorn/uwsgi działają domyślnie jako WSGI (sync). Musimy zatem zastosować np. Daphne (oficjalny serwer ASGI od Twisted) lub Uvicorn (bardzo wydajny serwer ASGI na uvloop) jako workerów. Gunicorn ma możliwość uruchamiania workerów ASGI poprzez integrację z Uvicorn – np.
gunicorn myproj.asgi:application -k uvicorn.workers.UvicornWorker
. Jeśli zapomnimy o serwerze ASGI, naszeasync def
widoki i tak będą działać, ale bez sensu – każdy request będzie miał swoją oddzielną pętlę zdarzeń (Asynchronous support | Django documentation | Django), co nie daje żadnej współbieżności między requestami. Podsumowując: ASGI deployment to mus, gdy chcemy iść w async. - Stosuj async tam, gdzie to ma sens – Nie wpadajmy w pułapkę "przerabiam wszystko na async bo jest nowsze/fajniejsze". Jak już wiemy, async najbardziej zyskuje przy I/O-bound. Jeśli nasz endpoint robi trzy zapytania do API, faktycznie zróbmy z niego
async def
i odpalmy te zapytania równolegle przez np.asyncio.gather()
– uzyskamy realny zysk w czasie odpowiedzi. Ale jeśli inny endpoint tylko pobiera rekord z bazy i robi drobną logikę, pozostawienie go jako sync może być prostszym i równie wydajnym rozwiązaniem. Django pozwala mieszać widoki sync i async w jednej aplikacji, więc używajmy async tam, gdzie daje przewagę, a nie wszędzie na siłę. Optymalna architektura może mieć 90% widoków nadal sync, a 10% specjalnych – async. I to jest OK. - Uważaj na kontekst i czas życia obiektów – W świecie async musimy przemyśleć np. korzystanie z obiektów request czy bazy w tle. Jeśli odpalimy własne zadanie asynchroniczne (np.
asyncio.create_task
) gdzieś w widoku, to ono może żyć dłużej niż sam request. Trzeba wtedy zadbać, by np. nie używać po drodze obiekturequest
(bo po zakończeniu widoku Django może go oczyścić/ponownie użyć). Pojawia się idea rozłączania kontekstu – to, co w świecie sync rzadko było problemem, bo wszystko działo się liniowo, tu wymaga myślenia: czy ta zmienna będzie jeszcze istniała gdy korutyna się wznowi?. Django co prawda radzi sobie z typowymi sprawami (np. ORM sam w sobie nie gubi się przy async iteracji, bo używa wewn. mechanizmów, by trzymać kursor zapytania), ale już własne globalne stany musimy chronić. - Testuj i profiluj wydajność – Zmiany architektoniczne warto poprzeć danymi. Uruchom aplikację pod ASGI i przeprowadź testy wydajności (np. JMeter, Locust) porównując z wersją WSGI. Być może w Twoim przypadku korzyść będzie olbrzymia – np. serwer da radę obsłużyć 2x więcej zapytań na sekundę – ale mogą wyjść niespodzianki (np. narzut async spowalnia proste requesty). Dokumentacja sugeruje, by samemu zmierzyć wpływ ASGI vs WSGI (Asynchronous support | Django documentation | Django). W szczególności, monitoruj użycie CPU i pamięci. Async potrafi zużywać więcej CPU przy wysokiej współbieżności (pętla intensywnie przełącza konteksty), ale za to znacząco mniej pamięci niż odpowiednik sync z tysiącem wątków. Wąskim gardłem może okazać się baza danych – jeśli masz jedną bazę, to i tak obsłuży ograniczoną liczbę zapytań na sekundę, niezależnie czy zrobisz je równolegle czy sekwencyjnie. Profilowanie (np. za pomocą Django Debug Toolbar, django-silk, czy po prostu logów) pomoże znaleźć miejsca, gdzie async nie daje efektu lub coś blokuje.
- Dobre praktyki programistyczne – Async bywa trudniejszy w debugowaniu, więc kod musi być czytelny. Stosujmy jasne nazwy (np. dopisujmy
_async
do naszych funkcji pomocniczych, by było wiadomo, że to korutyny), dokumentujmy kluczowe miejsca (# tutaj celowo używamy sync_to_async bo X
), dzielmy logikę na mniejsze funkcje. W testach korzystajmy z narzędzi jak pytest-asyncio lub wbudowane wsparcie Django (od wersji 4.0 Django TestCase potrafi obsłużyć async view, choć w tle i tak używa sync wątku). Pisząc testy integracyjne, symulujmy jednoczesne wywołania (czy np. nie pojawiają się race conditions). W skrócie: zachowujmy standardy czystego kodu, bo w async łatwiej o bałagan.
Identyfikuj i izoluj fragmenty sync – Przejście na async warto zacząć od audytu: które części naszego kodu/dodatków robią rzeczy potencjalnie blokujące? Np. używamy requests
do połączeń HTTP – to biblioteka synchroniczna. Rozwiązanie: przejście na bibliotekę HTTPX lub aiohttp, które mają wsparcie async (HTTPX integruje się idealnie z Django async views (Unlocking Performance: A Guide to Async Support in Django - DEV Community)). Podobnie, jeśli generujemy PDF synchronnie (np. wkhtmltopdf), rozważmy przeniesienie tego do zadania Celery zamiast robić inline. Tam, gdzie nie ma odpowiedników async, a musimy użyć danego kodu, sięgnijmy po wspomniane sync_to_async
. Funkcja ta potrafi wywołać dowolną funkcję synchroniczną w puli wątków tak, by nie zablokować pętli event loop (Asynchronous support | Django documentation | Django). Przykład:
from django.http import HttpResponse
from asgiref.sync import sync_to_async
def heavy_calculation(x, y):
... # coś co długo liczy synchronnie
async def my_view(request):
result = await sync_to_async(heavy_calculation)(40, 2)
return HttpResponse(f"Wynik obliczeń: {result}")
Tutaj heavy_calculation
wykona się w oddzielnym wątku (z puli), a nasz async widok nie zablokuje innym działania. Uwaga: Domyślnie sync_to_async
tworzy nowy wątek per wywołanie. Można go używać jako dekoratora z parametrem thread_sensitive
żeby kontrolować, czy może użyć istniejącego wątku, ale szczegóły wychodzą poza ramy – grunt, że mamy narzędzie ostatniej szansy. Analogicznie jest async_to_sync
do wołania kodu async z kontekstu sync (przydaje się np. w Channels).
Podsumowując, poprawne użycie asynchroniczności wymaga świadomego programowania. Nagrodą jest aplikacja, która pod obciążeniem reaguje lepiej i może zaoferować funkcjonalności real-time, ale trzeba dołożyć starań, by uniknąć subtelnych błędów.
Przykład – kod synchroniczny vs asynchroniczny
Pora na konkrety. Zobaczmy prosty przykład obrazujący różnicę w podejściu sync i async. Załóżmy, że chcemy w widoku pobrać dane pogodowe z dwóch niezależnych API i zwrócić je łącznie. Każde zapytanie trwa ok. 1 sekundy (symulujemy opóźnienie). Porównajmy implementacje:
Wersja synchroniczna:
import requests
from django.http import JsonResponse
def weather_view(request):
# Pobranie danych z dwóch API po kolei (blokująco)
resp_a = requests.get("https://api.weather.com/cityA") # ~1s
data_a = resp_a.json()
resp_b = requests.get("https://api.weather.com/cityB") # ~1s (czekamy dopiero po A)
data_b = resp_b.json()
combined = {"cityA": data_a, "cityB": data_b}
return JsonResponse(combined)
Tutaj czas obsługi requestu to ~2 sekundy, bo musimy kolejno czekać na oba żądania zewnętrzne (1s + 1s). W tym czasie wątek nic innego nie zrobi. Jeśli przyjdzie 100 takich zapytań jednocześnie, to potrzebujemy wielu workerów, a użytkownicy będą czekać 2 sekundy na odpowiedź.
Wersja asynchroniczna:
import httpx
from django.http import JsonResponse
async def weather_view(request):
async with httpx.AsyncClient() as client:
# Wysłanie obu zapytań niemal równocześnie
task_a = client.get("https://api.weather.com/cityA")
task_b = client.get("https://api.weather.com/cityB")
resp_a, resp_b = await asyncio.gather(task_a, task_b) # czekamy na obie równolegle
combined = {"cityA": resp_a.json(), "cityB": resp_b.json()}
return JsonResponse(combined)
Tutaj dzięki asyncio.gather
oba zapytania wykonywane są współbieżnie. Łączny czas obsługi to ~1 sekunda (zamiast 2), bo czekamy równolegle na A i B. Co więcej, ten sam worker w tym czasie mógłby obsłużyć inne requesty, jeśli by przyszły (bo nasze await
zwalnia pętlę event loop). Dla 100 jednoczesnych użytkowników wciąż łączny czas każdego z nich to ~1s, a nie 2s, i nie musimy mieć 100 wątków – te 100 zadań może obsługiwać np. kilka wątków async (w praktyce nawet pojedynczy event loop dałby radę, choć dla CPU rozłożenia można mieć np. 4 workery async, które i tak łącznie obsłużą setki połączeń).
Różnica w kodzie: Widzimy, że wersja async jest nieco bardziej złożona – trzeba użyć innej biblioteki HTTP (requests nie wspiera async), mamy kontekst menedżera async with
, trochę nowej składni. Jednak z punktu widzenia użytkownika zysk jest duży. Oczywiście, jeśli zamiast dwóch API mielibyśmy pięć, zysk async byłby jeszcze większy (5s vs 1s). Natomiast gdyby to zapytanie do API było jedyne, to asynchroniczność nie przyspieszy pojedynczego requestu (1s to 1s), ale pozwoli obsłużyć więcej takich zapytań jednocześnie bez korkowania serwera.
Przykład z bazą danych: Rozważmy inny scenariusz – chcemy pobrać listę obiektów i ich powiązanych danych z bazy oraz zewnętrznego API. W trybie synchronicznym mogłoby to wyglądać tak:
def data_view(request):
users = list(User.objects.filter(active=True)[:100]) # zapytanie do bazy (blokujące)
external_data = requests.get("https://ext.api/data").json() # zewnętrzne API (blokujące)
# ...połączenie danych...
return JsonResponse({...})
Tutaj i baza, i external API blokują. W async zrobimy:
async def data_view(request):
# pobranie danych z bazy async (np. używając async iteratora)
users = [user async for user in User.objects.filter(active=True)[:100]]
# pobranie danych zewnętrznych async
async with httpx.AsyncClient() as client:
external_data = await client.get("https://ext.api/data")
external_json = external_data.json()
# ...połączenie danych...
return JsonResponse({...})
Zapytanie do bazy async for
sprawi, że event loop może obsługiwać inne rzeczy podczas wyciągania danych z DB (choć tu uwaga: jeżeli DB nie obsługuje asynchronicznie zapytań, to i tak pewne opóźnienie będzie – Django ORM pod spodem i tak użyje wątku, chyba że korzystamy z natywnego async drivera, np. do PostgreSQL można użyć asyncpg w przyszłości). Mimo to odciążamy główny wątek. Zapytanie do API jest awaitowane, więc również nie blokuje.
Podsumowanie przykładu: Kod async jest bardziej "rozproszony" (najpierw zaplanowanie zadań, potem await
), ale pozwala skrócić czas oczekiwania i zwiększyć przepustowość. W prostych przypadkach różnica będzie minimalna, ale przy wielu operacjach I/O oszczędności czasu sumują się.
Ważne jest też zauważenie, że jeśli zamiast I/O mielibyśmy coś obliczać lokalnie, np. zsumować wielką listę liczb, to w async nie zyskamy – musielibyśmy i tak wykonać obliczenie od początku do końca (chyba że rozbijemy je na kawałki i między kawałkami zrobimy await asyncio.sleep(0)
żeby oddać kontrolę – to sztuczka na współbieżność CPU-bound, ale rzadko stosowana, raczej w takich wypadkach idziemy w wątki albo procesy).
Zewnętrzne narzędzia uzupełniające asynchroniczność w Django
Na koniec warto wspomnieć o narzędziach spoza core Django, które uzupełniają lub ułatwiają asynchroniczne podejście:
- Celery – Czyli niezastąpiony kombajn do zadań w tle. Celery to message queue i worker pozwalający wykonywać kod Pythona poza kontekstem obsługi żądania webowego. Choć Celery działa tradycyjnie na osobnych procesach (nie współdzieli pętli asyncio z Django), to idealnie uzupełnia model async. Jeżeli mamy duże zadanie do wykonania asynchronicznie, np. wysłanie 100 maili, przetworzenie video – zamiast próbować to upchnąć w asyncio (co nie pomoże na CPU, a może blokować nasz serwer), lepiej wrzucić to do Celery. Wątek/worker Celery wykona zadanie, a my w Django od razu zwrócimy odpowiedź że "zadanie przyjęte". To nic nowego – Celery istniał zanim Django miało async – ale teraz myślenie asynchroniczne jest jeszcze ważniejsze. Celery integruje się z Django bardzo dobrze (odczytuje konfigurację, używa Django ORM jako opcjonalnego backendu wyników itp.) (Unlocking Performance: A Guide to Async Support in Django - DEV Community). Warto go użyć zawsze, gdy pewnych rzeczy nie musimy robić w ramach request-response. Np. użytkownik kliknął "generuj raport" – możemy odpalić zadanie Celery generujące PDF i powiadomić go mailem gdy gotowe, zamiast kazać mu czekać 30s patrząc na spinner.
- Dramatiq – Lżejsza alternatywa dla Celery, zyskująca na popularności. Dramatiq pełni podobną rolę – kolejka zadań do odpalania w tle – ale jest reklamowany jako prostszy w użyciu i niezawodny zamiennik Celery (Task Queues - Full Stack Python). Wspiera jako brokery m.in. RabbitMQ i Redis. Dla Django istnieje pakiet django-dramatiq, który ułatwia integrację (automatyczne załadowanie zadań, management command do uruchamiania workerów itp.). Zaleta Dramatiq to mniejsza liczba zależności i często prostsza konfiguracja niż Celery. Jeśli Celery wydaje się zbyt skomplikowany, warto rzucić okiem na Dramatiq. Pod kątem asynchroniczności – tak samo jak Celery – działa poza głównym serwerem, ale odciąża naszą aplikację od długotrwałych prac.
- Django Channels – Omówione wcześniej w kontekście WebSocketów. Channels można też użyć do zadań w tle typu WebSocket – np. mechanizm Channel Layers pozwala komunikować się między procesami (np. powiadomić z workera Celery do serwera web, że coś jest gotowe – by pchnąć update po WebSocket). Channels w wersji 3+ są w pełni kompatybilne z nowym async Django (bazują na ASGI). Jeśli planujemy intensywnie korzystać z połączeń stałych, rozważmy Channels zamiast własnych rozwiązań.
- Inne kolejki zadań – W Pythonie jest cała gama innych tooli: RQ (Redis Queue) – prosty queue oparty o Redis, Huey – minimalistyczny, django-q, itp. Każdy z nich realizuje podobny cel: asynchroniczne przetwarzanie zadań poza cyklem żądanie-odpowiedź. Wybór zależy od potrzeb – Celery jest najpotężniejszy (ale i trochę złożony), RQ najprostszy (ale mniej funkcji). W kontekście Django async – one wszystkie w sumie działają tak samo jak dawniej, bo są niezależne od tego czy widoki są async czy nie.
- Biblioteki asynchroniczne – Warto wspomnieć, że do efektywnego kodu async potrzebujemy też narzędzi async do różnych zadań: wspomniany HTTPX do HTTP, ale też np. asynchroniczne klienty baz danych (pojawił się np. Databases dla SQLAlchemy, może w przyszłości doczekamy się w Django podobnego?), biblioteki do async plików (aiofiles), do usług chmurowych (boto3 ma częściowe wsparcie async), itd. Pisząc Django async, naturalnie będziemy sięgać po te async-friendly libraries. Pamiętajmy, by nie mieszać w async code nagle wywołań
requests.get()
czytime.sleep()
– to powinny byćawait client.get()
iawait asyncio.sleep()
.

Wydajność: czy async naprawdę przyspiesza? Na co uważać przy migracji?
Skoro mowa o async, nie sposób pominąć kwestii wydajności. Czy przejście na Django async przyspieszy naszą aplikację? Odpowiedź brzmi: to zależy. Kilka podsumowujących punktów, które warto wziąć pod uwagę przy podejmowaniu decyzji i migracji:
- Jeśli Twoja aplikacja jest I/O-bound i ma wiele równoczesnych połączeń – prawdopodobnie zobaczysz znaczącą poprawę przepustowości. Np. API obsługujące wielu klientów czekających na odpowiedzi z zewnętrznych serwisów może obsłużyć wielokrotnie więcej requestów jednocześnie na tej samej maszynie dzięki asyncio. Użytkownicy odczują mniejsze opóźnienia w sytuacjach obciążenia. To są scenariusze, dla których async był projektowany.
- Jeśli aplikacja jest CPU-bound lub każde żądanie jest krótkie i niezależne – async może niewiele zmienić, a czasem wręcz nieznacznie pogorszyć latency pojedynczego żądania (narzut event loop). Dla prostego CRUD-a, który wykonuje jedno zapytanie do bazy i zwraca HTML, różnica między WSGI a ASGI będzie praktycznie niezauważalna dla użytkownika. Może się wręcz okazać, że WSGI z klasycznym wielowątkowym podejściem sprawuje się równie dobrze lub lepiej w danym środowisku. Jak wspomniano, odnotowano przypadki, gdzie czysto synchroniczne operacje pod ASGI były nieco wolniejsze (kilka-kilkanaście ms) niż pod WSGI (Huge performance difference when using ASGI and WSGI - Async - Django Forum) z powodu narzutu. Trzeba więc mierzyć i nie zakładać cudów.
- Wąskie gardła się nie zmieniają – Async nie przyspieszy bazy danych ani zewnętrznego API. Jeśli DB wyrabia 100 zapytań/s, to nawet jak odpalisz 1000 na raz asynchronicznie, to one i tak będą czekać w kolejce na DB. Możesz co najwyżej lepiej wykorzystać czas oczekiwania na DB, robiąc w międzyczasie coś innego. Upewnij się, że po migracji na async, np. baza nie zacznie być obciążona gwałtownymi burstami zapytań (bo np. wcześniej robiłeś 5 zapytań sekwencyjnie, a teraz polecą naraz – obciążenie szczytowe DB wzrośnie). Często i tak skalowanie wymaga optymalizacji innych warstw (cache, lepsze indeksy, replikacja bazy). Async daje potencjał, by zapytania z naszej aplikacji nie były już wąskim gardłem, ale reszta systemu musi nadążyć.
- Profiluj pod kątem przełączania sync/async – Jeżeli części aplikacji zostaną sync (np. middleware), rozważ ich eliminację lub asynchroniczne odpowiedniki. Każdy dodatkowy thread jump to narzut. Być może migrując aplikację warto również zrefaktoryzować pewne rzeczy – np. uprościć łańcuch middleware, usunąć te, z których nie korzystamy intensywnie lub poszukać ich async wersji. Django stara się minimalizować liczbę przełączeń (Asynchronous support | Django documentation | Django), ale konfiguracja każdej aplikacji jest inna.
- Stopniowa migracja – Na szczęście Django umożliwia stopniowe wprowadzanie async. Możesz zacząć od jednego endpointu krytycznego dla Ciebie (np. ten który obecnie ogranicza throughput aplikacji) i uczynić go async, resztę zostawiając po staremu. To dobra taktyka – pozwala zaobserwować realne korzyści i problemy w kontrolowanym zakresie. Nie musisz przepisywać całego projektu na raz. Możesz również przełączyć deployment na ASGI z minimalnymi zmianami kodu (wszystkie widoki dalej sync) i zmierzyć różnicę – jeśli overhead będzie pomijalny, droga wolna by wprowadzać widoki async tam, gdzie trzeba.
Na koniec dnia, asynchroniczność to narzędzie, nie cel sam w sobie. W Django potrafi zdziałać wiele dobrego, ale wymaga ostrożności i zrozumienia. Wykorzystana właściwie – pozwoli tworzyć skalowalne, responsywne aplikacje webowe, łącząc zalety dojrzałego ekosystemu Django z nowoczesnym podejściem współbieżności. Wykorzystana pochopnie – może narobić bugów i niekoniecznie poprawić wydajność.
Czy warto? Jeśli masz aplikację, która cierpi na blokujące operacje I/O lub planujesz funkcje realtime – zdecydowanie tak, warto rozważyć async. Jeśli Twoje Django działa błyskawicznie na obecnym ruchu, a jedyne opóźnienia to logika CPU (np. generowanie PDF) – async tu niewiele pomoże (lepiej Celery). Jak zawsze, wybór technologii powinien wynikać z realnych potrzeb.
TL;DR: Django oferuje async views, async ORM (częściowo), async middleware i wsparcie ASGI. To daje nowe możliwości skalowania i funkcjonalności (WebSockety!). Ale async to nie srebrna kula – trzeba uważać na ORM, GIL, kompatybilność kodu. Stosuj async tam, gdzie czekasz na I/O, a nie dla samej mody. Dobre praktyki i narzędzia (Celery, Channels) pomogą wycisnąć z Django asynchronicznego to, co najlepsze. W efekcie możesz zbudować nowoczesną aplikację Django, która pod maską robi wiele rzeczy naraz, a na zewnątrz nadal cieszy prostotą i solidnością. (Asynchronous support | Django documentation | Django)