Django Through the Years with Andrew Godwin
Published November 3, 2022
This video features Andrew Godwin at DjangoCon US 2019 in San Diego, California, USA.
DjangoCon 2019 - Just Add Await: Retrofitting Async Into Django by Andrew Godwin
Writing async code from scratch is hard; trying to add it into a large, existing framework is harder. Learn about the problems we face trying to make Django async while maintaining backwards compatibility, as well as the problems maintaining hybrid sync-and-async Python codebases in general.
This talk was presented at: https://2019.djangocon.us/talks/just-add-await-retrofitting-async-into/
LINKS:
Follow Andrew Godwin 👇
On Twitter: https://twitter.com/andrewgodwin
Official homepage: http://www.aeracode.org
Follow DjangCon US 👇
https://twitter.com/djangocon
Follow DEFNA 👇
https://twitter.com/defnado
https://www.defna.org/
Intro music: "This Is How We Quirk It" by Avocado Junkie.
Video production by Confreaks TV.
Captions by White Coat Captioning.
Andrew Godwin explains why adding asynchronous support to Django is more than appending `await`: Python’s coroutines require cooperative scheduling, and synchronous code can block the event loop unless it is safely isolated in threads. Django must preserve its stable, backwards-compatible APIs while bridging synchronous and asynchronous code, third-party libraries, database drivers, middleware, request handling, and thread-sensitive behavior such as SQLite. He describes an outside-in plan: add ASGI support, make the handler, middleware, and views async-capable, and then retrofit the ORM, while keeping synchronous Django fully supported. The central argument is that async should be an optional tool rather than a wholesale replacement: most Django code can remain synchronous, while async views can enable highly concurrent I/O-heavy applications and open Django to the wider async ecosystem.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Thank you very much, Katie. So yes, good morning everyone. This is it says just add a weight. It's maybe not quite as easy as the title belies. But a brief introduction to me first of all. I am Andrew Goldwyn. Some of you may know me. I've worked on things like Django migrations and South and channels and I've been around Django a while My day job is working out of Embrite trying to fix their wonderful scaling issues and all manners of things at such a large Django website. And if you want to find me on Twitter or my personal website and my list of national parks I've been to, you can go there. But please Later. Um but what are we here for? We are here for one thing, which is that I can fit Django on one slide and it's not Flask. No, I'm joking. Um it is for this. It is for The goal that when I sat down five, six years ago and thought about what an async Django could look like, it's this.
It's that you can sit down and write things that looks like normal Django. that you can immediately understand even if you've never seen asynchronous code before, but that runs asynchronously. And while Just presenting this to you in the abstract by itself may seem either super easy or super hard. There's a very detailed plan behind this and the plan to bring async into Django, as Amber sort of alluded to earlier in in her keynote So I'm going to go through as well as I can in the time I have the sort of basic async landscape. I'm then going to go through the particular problems that comes from being a big mature framework like Django. And then I'm going to go in depth, a deep dive into what it means to actually implement some of this in Django's requests paths and ORMs and other bits like that.
So first, let's talk a bit about async in brief. You heard from Amber the basic premise of async, the idea that threads are preemptive. But let's go into a bit more of what that means in Python specifically. So Python has a threading library. You can just import threading and run threads, but it's a lie. Python threads are not actually threads. It's not a thing that runs on multiple processor cores. It's just an implementation of what's called time slicing in Python. When you have threads, it just takes each of the threads you have and runs them in equal time slices No matter if they've got work to do or not, even if this thread is idling, it will still be context switch to go, oh, it's idling, and context switch away from it again. By comparison, coroutines are cooperative.
They only uh yield and give back to the main control of the event loop when they hit an await. What that means is if you're not doing anything, you never even get hit. But if you're doing a lot, you actually can suck up all that different time. and kind of waste it. The cooperative is part of the name. If you are an uncooperative coroutine, you can ruin everyone else's day. That's an important thing to think about as we sort of go further down into when I talk about dangers of asynchrony. And so we have these two different lands. And traditionally people in Python have sometimes used threads. If you've gone near the threading module, you may have some kind of appreciation of how difficult it can be and how dangerous it can be to go there. Coroutines are different. In some ways they are much safer, and in some ways they are harder to think about.
The key thing coroutines need is an event loop. Now Amber did mention this. This is called the brains of the operation. It's the thing that listens on the OS sockets that works out, oh, there's a byte incoming from the network. It's where the program goes when a coroutine exits to work out what it's doing next. You can imagine that every coroutine sort of runs, gives back to the event loop, the event loop goes, oh, there's a new thing over here on this socket. Who's who has that socket is this coroutine and then gives control to that coroutine. That's kind of what it does. And the fun thing is the event loop is a thread. And this kind of becomes the sort of I had this dawning moment about two years ago. I was like, oh, that's awful. When you realize that you can have both threads and coroutines and you can have multiple event loops in multiple threads. Don't do this So
here's a sort of visual illustration, right? So say I'm running a standard normal Python application that is synchronous by default. I can spin up an event loop in my synchronous application and turn that synchronous thread into an asynchronous hosting thread. And basically what you do is you call the event loop and it just blocks wherever you call it from. It sort of absorbs that thread and uses it until it returns at the end of it. And so you can see here that like in this diagram, you can basically take that synchronous thread and turn it into something that runs multiple coroutines inside it. But then of course You may want to call synchronous code, maybe legacy Python code, or things that just need to be synchronous or they're simpler, from your asynchronous code. And so now you have to call you have to make a separate thread and then call the synchronous thread from your asynchronous thread.
And this gets really tricky and we'll see how later. But just appreciate the idea that like when we talk about threading in Python, a thread is either running a event loop and it's an asynchronous thread, or it is not running an event loop and it's asynchronous thread. And that's kind of the the dual mode you can have. Each thread is one of the two. Now why don't we just use threads? Threads are slow. As I mentioned, it's not a real like OS level threading implementation. And so what ends up happening is the more threads you add The more Python just naively cycles into each of the threads, and it gets slower and slower and slower. If you try and run 10,000 threads on Python, it will just crumble under the weight of context switching constantly and never get any work done.
And so the goal here is we want to have async. As Amber alluded, async is fast, as long as you're I. O. bound. Luckily We write websites. Websites are pretty much I. O. bound the entire time. Either you're waiting for the user to upload stuff from HTTP, or you're talking to a database, or you're talking to an API. Like all these things are I. O. Very few sites in the world spend their time literally locked in the CPU doing pure calculation. Template rendering, for example, is usually that. But even in Django, template rendering often calls a database. And so you you've there immediately got some I. O. in it. And the other thing to consider is while async may seem like a wonderful solution, there's a slight issue with the way Python was designed. And this is no fault particularly of the Python core team, like it's just the way it evolved.
Because of the way Python async came out of yield and yield from, there's a long history there. Async functions are different to synchronous functions. One function cannot be both of them. This is really awkward when you're designing APIs and we'll touch on this later, but really what it ends up being is that you can't have one function that you can call from a synchronous context and that you can call from an asynchronous context. And to add to that problem, if you do it wrong, it's dangerous. If you're in an asynchronous thread, and as I mentioned, it's sort of cooperative. So it's your job to do the minimum minimal amount of work and then yield when you're sort of waiting for I. O. If you have naive synchronous code in that coroutine, that Synchronously goes and does a blocking fetch, you've just locked up the entire event loop for that second or two seconds that that remote web call takes.
And the event loop cannot cut you off. It cannot come in and stop you. You have literally just ruined the contract of being cooperative. And so if you are calling synchronous code in in asynchronous land, it is dangerous. And also it's very easy to do by mistake. If you just forget to put an await in, that can be a problem. And so these are all problems we face. And like here's a illustration of that problem, right? Like I have an asynchronous thread. I accidentally make one of the coroutines synchronous and nothing happens for a full second. What you want is to do the thing I mentioned earlier, where you go, ah, I have some synchronous code. I'm going to shove it into a separate thread. And then I can return control to my event loop, let the event loop do other stuff in the meantime, and when my thread is finished, it will then signal the event loop to come back and resume my coroutine.
That's the pattern you kind of want to do. If you haven't got the impression yet, it's very complicated. And as we'll get into, async code is great, but I encourage you, even when Django has full support for it. Write synchronous code first, understand your logic, understand where it falls down, and then take the parts of the optimization and take those asynchronous. And that's kind of the philosophy you'll hear throughout this talk is that asynchrony is great and it should be an optional add-on. So let's talk about Django. Django is a big framework. I love it. It's been a big part of my career in programming. And one of the reasons I like it so much is that it's very stable and predictable. And this of course brings with it a whole host of problems when we're saying, well, we want to totally change the
one of the paradigms it operates on As I mentioned, a function cannot be both synchronous and asynchronous. And this immediately presents a problem when you think of any API in Django. So let's take the caching framework. Caches are usually on the network. Let's say I'm talking to Memcache or Redis. I have to go do a connection, ask for the get, get it back. It's usually like, you know, tens of milliseconds, which as compared to normal CPU time is an eternity. So I should be being async here. But while Django has the top one here, it has the normal cache. get, we can't make the bottom line work. We can't have cache. get also be asynchronous compatible. We can have a cache. get async and have two different functions But as you can imagine, that makes for a very ugly API.
Like the proposal we have is literally you do cache. async. get and try and namespace them under that kind of thing. But even that's a little bit um ugly and a little bit extra code to maintain. And that's just the start. Django is built on a series of incredible third-party libraries from the Python community, specifically databases. Things like the memcache library I just talked about, things like the Python imaging library or Pillow is in is is now. These are all libraries that were written in a synchronous world. And for things like databases, we have a standard called DBAPI2. It's synchronous. There is no DB API 3 that's asynchronous. There is no standard way for all the different async, MySQL, and Postgres libraries to present themselves to Django.
Even sleep is different. Like it's not even the same thing. It's literally a different import from a different place. Standards are really good and one of the problems you have when you sort of wander into this new world is that they're not there anymore. And the principal one of these, of course, was how Django presents itself to a server, Whiskey. WSGI, as it's often pronounced better, is a wonderful standard that has maybe been one of the prime reasons Python is so successful as a web language. Because you can pick any framework and you can pick any server and they will talk to each other. Like I can take GUnicorn and I can run Flask on it, I can run Django on it, I can run anything else on it, I can write my awful little app I wrote in a weekend as it's that its own framework.
And we just didn't have this for asynchronous stuff. There are a few proposals from different servers and like tight server bindings, but I was around before WISGI was standardized and I remember the days of oh this web framework has to use this web server. You have no other option. And like that's kind of you sort of took them as a bundle deal. So now we have ASGI. And I will not go into ASGI in full. That is a probably another full 45 minutes of presentation that would probably bore you all to death. But it is a whiskey-like It is as close to WISGI as we could get it, but still being asynchronous. And the key thing is it is You make an object, a callable, that you give a scope, which is sort of like the environment like, oh, here's your row client address, here's your headers, here's things like that, and you get a send and receive callable.
And what that means is unlike WSGI, where you just get given the input when you're called, you can sit there and receive packets, you can do processing between them, you can send back multiple packets. It is full duplex, as we say, in networking. And this is really useful for things like WebSockets, but also for HTTP as well. Like modern like HTTP2 and 3 features require more and more communication between the client and the server than just a simple request and response. And that's just not all. The language itself kind of works against us too. Some language features that Django relies upon heavily do not have an async equivalent. You may all be familiar with the fact that you can, for example, do
dot something on a related model and get the idea. For example, here we have instance, you can do dot foreign key, then you can do dot name. And Django happily in the background. Pause, go create the database, find the instance, load it into memory, and then give you the name. Now while we can do an await version of say objects. get or objects. filter, that works fine. What we can't do is we cannot do asynchronous attribute access. If you are in an async thread and you call something like . name and it starts running the ORM You've just run synchronous code. You've just blocked that entire thread for half a second maybe and you've broken the contract And that's really tricky. And so I like and this is one of the things that like brought me into Django.
Like I saw Django like, oh I can just traverse things easily via dots. And we'll talk about how to solve this one later. But it's really one of those little tricky things. And kind of finally, uh, threads do matter. While most code in Python is generally thread safe, some things are not, and SQLite is one of those things. Django and SQLite kind of come together like most test suites are run, at least at the small scale, in SQLite And if you take SQLite and you make a connection in one thread and then try and use it from another one, it will just complain at you and explode. And so even though we're trying to get rid of threads, we still have to consider them because of the fact that asynchronous code is in itself running in a thread.
And if you call synchronous code, it runs in a different thread. And on top of all of this, we have to be backwards compatible. Django has been, with very, very few minor exceptions, backwards compatible since 1. 0. Like every release you can go, you can open the release notes, you can go, oh, okay, I see. I just these few small changes and they're declared in advance and I I can get there. What that means is we have to keep that. I can't just give you all a version of Django that's like, oh, this is totally different. You have to rewrite from scratch. Because quite rightly you should all say, no, Andrew, that's stupid. We're not going to use this. And so we have to make sure that all of these features are accounted for and that when you take an existing Django application and move it into this new version of Django and add one async view, that all of the rest of it runs perfectly fine still.
And so we have this problem, like async has to add Django. Like we can't replace a wholesale. We have to play like, well, you get to add one or two async views to your current project, but the rest of it still runs fine. Things should look familiar, they should still feel Django-ish. You should still have things like dot objects and the filters and the models and the views should work the same way and the middleware should seem familiar. And they need to be safe. Django has always stopped you from essentially shooting yourself in the foot. We try and make sure that it ships with Things that are safe by default. Debug is the one in exception to that and we really try and make you turn it off in production. But in general, it's very like things like The authentication framework has things like constant time password comparison that people don't really think about until they get attacked with a password uh break. And this is all the problem of like what it takes to take Django
and really pull it apart to make it A. So let's look at some of those actual things in detail. So if you have never like sort of dived into the Django internals and give you a brief overview of how Django is laid out. This is obviously a very simplified version of Django, but essentially Django has a couple of different a couple of different pieces. It's built around a recr a request path. Where you have a WSGI server that calls a thing called the handler, which sort of translates WSGI into what Django thinks of as a request, the request object you all use. The handler then sort of talks to and runs the middleware, it then talks to the URL router, and it ends up with a request that's been through middleware
and a view function. And it takes those and it runs the view function with the request. And the view function is then of course supplied by you, the wonderful developer. And then you can call the RRM, you can call template, you can call forms, and then when you return a response, the handler takes the response object. It decodes it back into the network um sort of WSGI layer and it hands it back to the server. And this is sort of kind of useful because what we can do is we can take one of the key things I've learned about doing big rewrites. Which is a phased approach. In particular, we can go outside in. And this is kind of what we I sort of alluded to when Amber was talking about like what's in Django 3 and 3. 1. There's three phases, sort of I've broken it down to simply in this. The first one is having support for talking not just WSGI, for talking to a different back-end
protocol, ASGI. Phase two is making that core part of the request path, the handler, the middleware, and the views all async capable And the third phase is taking the ORM, the thing I mentioned at the top of this talk that's probably the best use of async that you get the most efficiency out of, and making that async as well. And so these three each have their own benefit. The first phase, ASGI support, is maybe the least useful to you, the end developer. But it's a really important foundation for us to have in place. So that not only do we have the ability to build phase two, but also we should we sort of tell the ecosystem, hey, we are going to support ASGI when the next release comes around. you should probably think about making sure you're going to work with this.
And some servers are already thinking about adding support beyond just the ones we have now. And in terms of timing, um this has shifted in the last couple of months. My initial goal was Django 3 for both the first two phases Uh but it's been quite a few months, let's say. And so um Django 3 does have ASGI support. When that releases in late later this year, um it will run against an ASGI server. Phase two, which is async views and middleware, did not make the cut, but that should almost certainly make it into Django 3. 1. And then the RM work is the largest and most difficult and most unbounded part of this. My hope is it makes it into Django 3. 2, but I'm not gonna hold myself to that at this early stage. And the other key thing is, and I've learned for big re-reuts
is you have to plan for failure. If you've seen some some of my other talks about engineering, like planning for failure is very important. Even if we cancel at any point of this project, we have concrete benefits. If we cancel after phase one, which we haven't done, we still have the support to like have somebody else come in in future and make part of Django async. If we cancel off to phase two and just have async views, that in itself is a huge benefit. People can now go and use things like async requests libraries and go and talk to things themselves. They can't use the ORM the same way, but they can do a lot of API calls. easily and a lot of modern web development is API driven. And of course, if we do like half the ORM, that's still going to bring performance improvements. So let's talk about each of those phases in a bit more depth and what it means to be outside
in. So first of all, ASGI. And a fun bit of history here. Django predates WSGI. When I first came to Django in around the 0. 96 era , WSGI was this new sort of thing for like, oh, we could have a standard that wasn't just tied to one server. And one of my favorite uh examples of the almost weird furori around this back then, um, historical note people kind of thought Django was a bit full of itself back then. This is one favorite quote from our very own James Bennett um in a 2006 blog post called Django and NIH. As he says, just so you know, Django is a smug, arrogant framework that doesn't play nice with others. Or at least that's the impression you'd get from reading the rants.
Um I want to bring this up because like there was a time when WSGI was in its own way controversial, right? That there's there is history here. And what is wonderful about this is the fact that Django predates WSGI, we kept the indirection between WSGI and Django in there. Like I sort of dusted it off after basically a decade and went, oh, we can still fit a new protocol in here because we let we left this junction point like j it just we just had WSGI hooked up to it and we removed the old one years ago. But it was still there. And that's one of the wonderful like history things that has come full circle and lets us make things more easily. And there's other things too, like We have our own request and response objects. Again, this is a useful thing because we can adapt those to either protocol.
We have our own handler classes, as I said, the perfect place to put this new abstraction. And of course, we have custom middleware. For those of you who weren't around from Python 10 years ago, which is presumably most of you, There was this wonderful grand vision of WSGI middleware and thought you wouldn't need middleware anywhere else. You just write it all as WSGI apps and then Django would sit underneath all of it. And there were many reasons WSGI middleware is a bad idea, but it was a huge argument at the time, and the fact we have our own middleware means it actually is now easier to adapt. So what does this look like in a sort of zoomed-in level? Well, as I said, you have the server, and the server calls that handler class. The handler class basically Reads the environment, so in WSGI
is you get a big dictionary called Environ, which has like the headers and stuff in it. It takes that and maps it into a request object. And there is a subclass of request, and most of the logic there is in that subclass. So you give WISGI request the rec sort of the environment, it decodes a lot of it and does the headers itself. It also does things like if there's an input stream from uploaded file or post body, it takes that and wraps it in a file object and a few other bits and bobs to clean up the request and make it a single nice request object. Once it's done that and it has sort of a generic request, it then passes control to its superclass, which is called base handler. Now base handler has the generic part of Django, the bits that back in the day and now happen on both the different protocols.
Things like if you have an exception that doesn't get caught, it has a last chance exception handler. Things like if you set I want to have transactions around all my views, it's the thing that wraps transactions around all your views. And once it's done that, it also then is in charge of Taking your middleware setting, loading the classes, and then running the request through them in order. So basically it takes a request from its subclass, it runs through the middleware, it then ends up with a nice request that has all the stuff on it passes it to the Euro resolver, gets back a view, wraps it in a transaction if it has to, and then it calls the view. And you can take this whole idea and you can add in the ASGI side. And you can see here that like there is some duplicated code, of course, but we've reused
most of that right-hand side. All I've had to do is there's a new request subclass, which is There's a new handler which parses from the scope rather than the environment. But in general, like those two bits, they do their specific code and they both hand over control to the base handler. And in Django 3 These are the async parts. The ASGI server, the handler, and the request run natively asynchronously. And then as soon as ASGI handler passes control to the base handler, it switches and runs in a thread in synchronous mode. And that's kind of nice because it means I didn't have to touch the rest of Django. We literally just added the bits on the bottom left here in red, and then it just sort of worked. It wasn't quite that easy, but relatively it was fine.
One of the nice things as well is that ASGI is deliberately mostly WSGI compatible. We sort of made sure when it was specified that there's a pretty direct mapping between the two of them. For example, things that you might have in the scope or the meta have direct comparisons in the scope in the ASGI. There are some more tricky parts. Uploaded files is particularly fun. This here is a precede, shortened version of what it takes to upload and ingest a file. In WSGI, you get given a literal file object. ASGI gives you events and it gives you like chunks of the files it streams in from the server. So if you want to, you can actually run before the file is fully uploaded. And but what that means is in Django, we have to take those chunks, write them to a spooled
temporary file, which is a Python 3 thing, like you can shove bytes into it, and it will sort of live in memory. If it gets too big, it pushes down to disk, but it gives you a file object. We rewind it to the beginning and we hand it to the thing. And all of that lets us just basically pop the control through to the existing base handler and not have to touch it. And what that meant was that first part that first patch, while it was big, was fully self-contained. Now you may remember I mentioned uh in the beginning of this presentation that async, cooling sync, is dangerous. And it is. But thankfully, it is a danger you can understand and contain. And what we have is a package called ASGIREF or ASGI Ref if you want. It has two things in it. A callable called sync to
async , another one called async to sync. And as the name suggests They map one world to the other. So for example, if you call sync to async, it takes a synchronous function, like for example, the based handler's re handle response, which is the thing we're trying to call. It wraps it in a thread pool, it handles exceptions well, it makes sure things like SQLite are happy, and then it runs the code. And while there's a lot of stuff in here, like things like thread locals work too. Because while Django tries to avoid thread locals, I think about half of all the Django sites I've ever seen shove requests into a thread local. So we have to handle that too. Like I I know it's convenient, but it 's fine. I'm gonna handle it. Um and so it does all that stuff for you. And like if you peer into the box, it is
honestly like slightly worrisome but you can close the box and just not think about it and just think about like well Andrew's done it and there's other people and they've got tests so it's probably fine. And that's that's the goal, right Like I want to make it so like you don't have to think about this kind of stuff every day that you can trust that there's a safe way of going between the two worlds. And as all of this, the result is Django 3 can speak ASGI. Like when it releases in later this year, it will do that. It unfortunately won't have async views, much to my my sadness. But it does set back groundwork. And if you really want to, it will let you write your own handler and start doing async things natively. And some some big companies do do that. But the really, really good part is phase two.
And I'm excited about this. Maybe you can't tell. So I said we're doing a phased one, sort of outside-in approach. And what that looks like is we take Phase one here and we sweep asynchrony through Django and make it further in. So in particular we rewrite base handler so it is also asynchronous. And so we can handle looking at a view and saying, is this view asynchronous? Which in Python is the is coroutine function callable. You can tell before you call it. If it's synchronous, we use sync to async and call it in a thread. If it's asynchronous, we can call it natively and have it run in our event loop. And what this means is now you've got a fully asynchronous path through from the ASGI server all the way through to your view, which means you have all those benefits of being natively async.
You can run very concurrently, you can do all that stuff. handle thousands and thousands of concurrent connections without running out of memory and without exhausting threads. With some caveats, we'll get to those The other fun thing is, while I've said there's only two things here, there's really a third part of this. Test client, the thing you use to call Django in tests, is the third entry point into base handler Like when you when you try and call and test, it doesn't actually do a full call through the Django stack. It sort of fakes a request object and then sort of quickly pops into the handler with a fake request object. And so what that means is we are basically forever gonna have synchronous code in either the test client or the WFGI handler calling into the newly asynchronous base handler.
And in fact, you may end up with a case where you have sync calling async calling sync. And like Django is not dropping support for WSGI. And we're not dropping support for synchronous views. Like those is those are staying around. And so we have this thing where like, well, we this is going to be a standard part of the way the code runs. And the naive ways of doing those two transitions, asyncao. run is the one Python recommends for going sync to async and thread pool the other way around. They are very naive. They do the thing I mentioned where the code on the far right there does not run in the same thread as the one on the left. And if you've got middleware that makes a database connection and then leaves it in the request for a thing to go and do later, it blows up in the most spectacular fashion. And the bugs are
the tests just it's awful. And it's not just SQLite, but it's the one you find earliest when you start on the test suite. And so I'm not going to tell you how I do it because it's very, very awful. But when you do this and when you call async to sync and sync to async, there is a mode called um thread sensitive. If you set that mode, it does some Let's say magic. This is not magic magic addition. It's kind of magic. And it runs both those synchronous things in the same thread. Again, there's a whole talk in this and you'll all hate me for it. If you want to go and be surprised, please read the code. It's tested really well, I promise. So remember what I said there's a caveat too?
Middleware. Middleware is really annoying. The old style of Django middleware is great. You had a you had a class, it had process view, process response. You call the middleware It did some stuff, it left. Then you called and then you ran the view, gave your response, you called the middleware, you left out of it. The new style is also lovely, but it has a problem in terms of asynchrony. The new style middleware, you give your middleware a function that says, hey, call this, get your response. And so what happens what that means is the middleware lives on the stack. And all the middleware is sort of suspended above the view while it runs. And when if the view returns or raises an exception, it runs back up through the middleware, zips through it, and if you can catch the exception, you're great. Now what this means is if you want to have
asynchronous, you've got asynchronous get response in the base handler, you've got asynchronous views, you may even have asynchronous middleware But if you've got just one synchronous middleware in that stack, and of course all the middleware that exists is by definition synchronous right now You have to use a thread. And we've got that one piece of synchrony, that one piece of blue in our lovely red end-to-end async. stack. And what this means is as long as you have synchronous middleware, you don't get the benefits of having m thousands of thousands of connections without using any threads. We're pretty sure the only way around this is gonna have to be to write all of Django's middleware to be natively async. Because the nice thing is you can run async middleware on sync views. That's perfectly fine.
But that's a really sort of tricky one of like we may have to say like, well, if you want to have this high concurrency, you have to limit your middleware. Or add some warning flags or detection about what kind of middleware is running. Again, this is sort of in progress. We're talking about this on the forum. If you have opinions on this, I'd love to hear them. But it's a really tricky one of what we want to keep compatibility with some restrictions, but make sure you get some of the benefits too. Of course, the main benefit of an async view is you can call stuff asynchronously and things like databases and stuff like that. And you still, even with a thread being used up, it's still faster, but it's not quite as good as it could be. There's lots of other problems too. Class-based views are a huge issue. The whole problem I mentioned with you can't have a callable, a function that's both synchronous and asynchronous, think of that, but the whole generic view stack
That's the problem there. We have some ideas again. Some of them are not so pretty. But we also might not do that in the first patch. Templates are fun too. Templates are synchronous in Django, and we're not going to touch that yet. But like you may think, but where's the template rendering in all the handler path? If you raise a 500 error or even a even the worse error errors that get caught right at the top. There's a template handler that it spins into action and renders that 500. And so a lot of the test failures earlier on was me missing that a piece of synchronous code was being called sneakily on the side by an error handler. So just go through and just add sync to async everywhere we could to like bring down the number of tests. And of course, tracebacks get kind of worse. All those sync to async calls take two or three lines up in every traceback.
So we're trying to look at a way to make that prettier, but it may just have to be a thing we live with. But the goal of all this is that we have async dev views in Django 3. 1. I have a branch where they totally work and all but two of the tests pass, which is why it's not merged. There's two tests that fail. But by all accounts you can take that branch, you can write one async dev view and do asynchronous things in it and it works perfectly fine. You can take an existing Django project, just add something to it, and it works. And that's always been the goal, right? And then we get to the hard part, which is the RM. Now, this is much less defined. I'm not going to spend too much time here because it's kind of beyond the horizon of the second phase. But I want to reiterate that API design and jack like being Django-like here is crucial.
You've got to have familiar but safe APIs. And what's nice is things like iterating over query sets, like Python did give us async 4. So we can just make query set work in both sync and asynchronous modes if you're iterating over it. . get won't work. You still need async. get. But we can do things like this that make it much easier to deal with. We probably can never do this. We can probably never have asynchrony work with calling and traversing models. But you should probably be using select related anyway. And so we'll be like if you want to use the ORM for an asynchronous function, you have to use select related is going to be basically the the c the conclusion there. And again this is all optional. You don't have to do that. this. And the same kind of phase approach works with the ORM. We start with a fully synchronous
one. We make query set have an async facade where it sort of looks async and you can do stuff. But it just runs the rest of the ORM and a thread pool behind it. And then eventually we'll try and make the whole thing asynchronous. And then we'll have a synchronous facade on top if you want to call it from, say, a synchronous view. In the meantime, before we get to all of that, we did add one thing in Django 3, which is that the Django ORM is now fully aware of async safety. If you try and call the RRM from an async context in Django 3, it will complain at you nonstop. It will be like, what are you doing? Why are you doing this? Please stop. Um and this is like the important first part, like we have to make sure it's at least somewhat safe. So that's the one thing we did get the RM done. But it does need a lot more research. Um this is one of the ones I'd love to have people
come and help with. Um I'm I have Never worked on the query side of the ORM, it is slightly terrifying and scary in there. And I'd love some help diving in and fixing it all up. And that's kind of sort of what it takes to dive into Django and break it apart a little bit. And I want to spend a little bit of time here at the end of the talk looking ahead and what this means for Django in the future and what this kind of big rewrite really means. So first of all, I really want to stress this. We are not removing synchrony from Django. Some things just don't need to be async. This is true of people's code. Like I personally believe 80 to 90% of the sites should be synchronous and only a core 10, 20% should be asynchronous. But also to Django, like the URL router is CPU bound.
It does not need updating to be asynchronous. We're just going to leave it as it is and not touch it Forms are perfectly fine, probably, unless you're trying to do validation. Small small uh problem there. But in general, there are parts we can just say, well, this is perfectly fine. We can just leave this as is and come back to it. And really for me, async views are the big cornerstone. Like when we get there, when we have a release of Django, we can go async death view, we have unlocked All of this from you have to understand weird inside Django stuff to you can go and do a weekend project where you make an asynchronous request library or an or your own small asynchronous ORM and use it with Django. We open up Django to the wider async ecosystem. And honestly, for me, this is the biggest part. The ORM would be great, but this really
opens up the ability to use all those wonderful libraries like Amber showed you in her presentation, for example. But we've got to be careful. Performance is a concern. We do things in Django to really trim down performance. Like if you've got a signal that has no listeners, it doesn't get called, for example. It's like a special edge case doesn't make it faster. And this is gonna cause some slowdown to normal synchronous Django. It is my personal view that if we cause too much slowdown, we do not do this. And we have to make sure that it is careful balance. Like we're not going to ship a Django 3-1 that makes your site run half as fast. Like 5%, 10% I might take, but any more than that's really pushing it. So we've got to be really careful about that and like what it costs us to do all those async switches. And it's not just technical, like there's people. I have at this point now done one and a half big rewrites of Django.
I have some experience with burnout and it's not good. It's not an initial to me. Like like these are projects that are very detailed. They are very specific. And it's not just about those of us who've been in Django forever doing it and buckling down and getting stuff done. That's not sustainable. What I love about this project is it's a perfect example of when you can get involved with Django. As an example, like we need to rewrite the middleware. But if I can give you like, hey, here is auth middleware. It needs to be made asynchronous. You have a precise spec of how it works, you have full documentation, and you know what asynchrony means, and you have a test suite. That is a very, very achievable and approachable goal for someone to contribute to Django for their first time And so my hope here is that we can take this work and not just spread it around and make it more
reliable and more sustainable, but also really bring on new contributors to help with this project. And maybe also have some of you learn the true horrors of async when you open the box. Of course, you shouldn't also not be paid for your time. Funding is a thing that I am certainly not the loudest voice on but we need funding and like more on this will come soon. Like I have plans that are forming on this particular front. But like if we are going to do a big project, we need to fund it and make sure it's not just people who have free time who can work on it. Like that's not good for anyone. And so really like there are async experts want to pay for their time, but even people who are new to JAIN and contributing, like I don't want you to take a financial hit or take away from like freelance contract work. to help contribute to Django.
I want that to be a thing that you can go, no, this makes sense to me. And like maybe it's a small, a small pay cut like I do for open source work, but like it makes sense. On top of all of this, of course, this is really big. Like this is maybe one of the largest changes in all of Django's history I can think of a few things when I've been around that have been very large patches. The aptly named Magic Removal was just when I arrived into Django, which as its name implies, removed a lot of magic. Things like settings used to automatically import. Remember right? This is literally, I'm really stretching here. Things just got magically imported to certain places and like sys. modules got fiddled with and it was it was unpleasant to say the least. But this is still pretty big, and we haven't done one of these in a while. And so Like
one of the things when I was thinking about async was like I need somewhere to talk to people. Like the Django Forum, which we're running now in a test phase It's partially launched because like I need somewhere to have like long conversations to everyone about like weird async stuff, but not clutter up and annoy Django developers with lots of weird async stuff that goes on forever and ever And like that's kind of the difficult part of like how do you as a modern open source organization do this kind of stuff? But I think finally the thing that really gets to me is why. People often come to me and go like, well Andrew, surely async is just a flash in the pan, right? It's the hot new buzzword all the kids, I'm not sure the kids talking about it, but all the kids are talking about. Why why does Django need it? And it's a good question.
There's lots of things I think that have been buzzwords or a flash in the pan or things that just aren't important. I think async is different. And obviously Amber's talk earlier showed a lot of those advantages, but we live in a world of applications where pretty much everything we do is I. O. bound. I can't think of more than one site I've worked on that was CPU bound, that sat there and like used 100% of its server. If you log into pretty much any Django like physical server or virtual server, but like onto the OS and run top, it is not at 100% CPU usage. It is full of memory. We are memory-bound because all those threads use a memory app and all the processes. But we are not CPU bound. It is my personal belief that a well-written asynchronous Django app could get a 5 to 10X
efficiency and performance improvement if it was heavily I. O. bound based on some numbers and some tests I've run. Obviously not for every app, obviously for different different things, different people. But it's such a huge advantage, even putting aside things like the fact that we live in a world of APIs and microservices, right? Like how many big sites Are not just Django anymore. Like you call like two different Amazon services and maybe a Google Cloud service and maybe over here there's something something from Azure and then there's like an API service over here and then there's three microservices. Like if you did all those in parallel You'd be a lot better off. Your users would have much lower page latency. And lower page latency is better user experience. At the end of the day, like asynchrony directly comes back to user experience. Like we want sites that are responsive and quick
and our users enjoy using. And like Django has always, in my mind, been about that, right? Django is there to give you the ability to write beautiful, amazing websites that people love. and that you do not put too much effort or understanding in. Like when we add async to Django, the goal is that you can understand as much or as little as you like, that you can use as much or as little as you like. To get those benefits. And that's for me really is the pitch. If you're curious about more, there are some links here. I'll post the slides on Twitter just after this if you want to not jot them down hurriedly or take a picture of them. But there is a blog post I have that goes more into synchrony versus asynchrony and what it means to switch threads and it has code samples and like some of the nasty things I talked about with threads.
Um DP9, which Amma mentioned, which is the novella length, I would say, um proposal to get async into Django. And then we have a page on the wiki which has sort of links to the forum and where to go and help and ideas of projects you could help with. And if you do want to help, please come talk to me here. Come to the Sprints. Or even just come on to the forum and chat and we'd love to hear from you. Thank you very much.
Python threads use time slicing, while coroutines cooperate by yielding control at `await` points. Coroutines can avoid scheduling idle work, but an uncooperative coroutine can block the entire event loop.
Discussed at 1:51Threads become increasingly expensive because Python spends more time context-switching as their number grows. Async is a better fit for mostly I/O-bound websites, where the program spends much of its time waiting on clients, databases, or APIs.
Discussed at 4:56Python's async functions are distinct from synchronous functions, so one callable cannot naturally serve both contexts. Django therefore needs separate APIs or adapters, such as an asynchronous variant of a cache operation, which complicates its interfaces.
Discussed at 6:30A blocking synchronous operation prevents the cooperative event loop from running other coroutines for the duration of that operation. The usual solution is to run the synchronous function in a separate thread and resume the coroutine when it finishes.
Discussed at 7:23ASGI is an asynchronous counterpart to WSGI that lets an application receive and send events, including multiple packets and full-duplex communication. It provides Django with a standard interface for asynchronous servers and features such as WebSockets.
Discussed at 11:18Django is taking a phased, outside-in approach: first ASGI support, then an async-capable request handler, middleware, and views, followed by asynchronous ORM work. Existing synchronous views and WSGI support remain available, so a project can add async incrementally.
Discussed at 17:40Django uses `sync_to_async` and `async_to_sync` adapters, with synchronous functions placed in a thread pool when called from async code. The adapters also handle exceptions, thread-local state, and thread-sensitive resources such as SQLite connections.
Discussed at 26:10Yes. An async-capable handler can detect whether a view is synchronous or asynchronous, run async views directly in the event loop, and call synchronous views in a thread. This allows an existing Django project to add individual async views without rewriting the rest of the application.
Discussed at 27:43A single synchronous middleware in the stack forces Django to use a thread around the request path, reducing the high-concurrency benefit of end-to-end async execution. Django may need to convert its middleware to native async, or impose restrictions or warnings about synchronous middleware.
Discussed at 31:35The planned approach is to provide an async facade over the existing ORM first, running its work in a thread pool, and eventually make the ORM itself asynchronous with a synchronous facade for synchronous callers. Async query iteration can work naturally, but implicit asynchronous relationship traversal is unlikely, so `select_related` may be required.
Discussed at 34:44Django is not removing synchronous execution: CPU-bound or simple components such as URL routing and most form handling can remain synchronous. The main goal is to make I/O-heavy paths, especially async views and eventually the ORM, work asynchronously.
Discussed at 36:15Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 15, 2026
Published July 14, 2026