What does switching to ASGI entail?

Assume that there is a Django project where everything is designed to accommodate WSGI. So, response times are as fast as they can get so that worker threads are not kept busy. To simplify things, you can even assume that only static HTML pages are served. (I know ASGI should help a lot when there is I/O stuff but I’m more interested in how requests are handled)

Now, let’s assume in this case I want to switch ASGI.

I know that unlike WSGI threads, ASGI threads can work on multiple requests. Thanks to this, you have access to some functionality such as efficient streaming with StreamingHttpResponse (without blocking the entire thread). This also means that overall, you’ll have better throughput since more threads will be available to process requests.

All of these are good and all for async views, but what happens when you have only sync views? Can ASGI still handle multiple requests in one thread? And how does it know which request is blocking?

In my mind -might be wrong here-, switching to ASGI in a sync project should come with near zero-overhead if not making the throughput any better. I know there is a context-switch penalty due to sync views but that should be very minimal right?

I made some naive tests using hey where I sent requests to a static page (using gunicorn with uvicorn and sync workers). And for some reason, sync workers were better at handling requests (i.e., the request times were faster and throughput was better). Did I do something wrong?

So…

Should I switch to ASGI? Is it feasible for sync applications to use ASGI? How do I properly test which one is better for my application?

2 Likes

Actually, as written, this isn’t exactly correct.

At the most fundamental level, ASGI runs everything in one thread. It relies upon the code being run to not tie up that thread any more than absolutely necessary.

So no, you won’t have better throughput overall until you reach the point where you’re handling enough requests that can be “event driven” instead of “process driven”. Handling 4 concurrent requests in 4 separate processes is going to take less time than handling those same 4 concurrent requests in a single thread if those requests are more CPU-bound than IO-bound.

You’ll only see a benefit if your views are spending enough time waiting for data rather than processing the data.

It will run those views in a separate thread from the thread running the event loop. (See Asynchronous support | Django documentation | Django)

This is not correct. A single-thread multi-process runtime is going to be faster for a single request than the overhead of an event loop. When you’re writing async code, say an await statement, you’re releasing control of the CPU back to the event handler. This creates overhead compared to the case where you’re keeping control of the CPU, possibly doing a “busy-wait”, while you’re waiting for your IO request to complete.

That really does depend upon your application. There’s no doubt that there are a set of applications, conditions, and circumstances that are greatly enhanced by an async approach.

However, I maintain that those situations do not exist for 99+% of all Django development being done. No, I don’t have any proof of that. I’m not aware of any survey or research to prove or disprove it. But the number of sites that deal with [“Facebook”, “X”, “Instagram”, “Amazon”, “Google”]-levels of traffic are exceedingly small. The number of sites that may aggregate data from multiple sources where parallel web requests or database queries would provide a tangible benefit is likely to be a much larger number - but I think that is still relatively small.

I think your first “test” is to evaluate your code and your project along with your (realistic) projections of the level of activity your project will see.

Again, async does not improve the throughput of CPU-bound tasks. It increases throughput in those areas where the tasks are otherwise waiting for IO to complete by allowing the CPU to work on other things.

9 Likes

That answer is exemplary @KenWhitesell :tophat:

1 Like

Great answer @KenWhitesell !

I’d like to provide concrete examples of when I’ve used sync views vs. async views:

When I started my largest Django app about 10 years ago, only sync views existed. The majority of these views fetch some data from a database (and maybe a Redis cache), does some in-memory data formatting, and then renders an HTML template. Mostly CPU bound, which works well for sync views.

However later I needed to introduce views that needed to make network requests to external APIs not under my control, which could involve a lot of wait time on I/O. These views are I/O bound. I implemented them as async views in the Sanic Framework because Django’s async view support was very new (read: immature) at the time. Today I would write those as async views in Django.

One final example: One view I wrote recently has the special job of exporting a lot of data from our database to S3 static file blobs. Both of these data stores are under my control and located near the app servers so the time of an individual I/O operation is minimal. However the export process requires that I make LOTS of calls to the S3 API to upload content. To make that fast I implemented the view as an async view so that I could make many upload API calls in parallel. (A sync view cannot easily/efficiently run I/O operations in parallel.)

I hope these examples are illustrative.

2 Likes

@KenWhitesell Thanks for the nice and elaborate answer!

So, if I understood right, ASGI is great if you have many I/O calls that actually await. Having I/O work in non-async views won’t help since they are executed synchronously in a separate thread.

Now I’m a bit confused by one statement found in Django documentation:

In some cases, there may be a performance increase even for a purely synchronous codebase under ASGI because the request-handling code is still all running asynchronously.

Do you know any examples of such ‘cases’?

1 Like

I don’t. I might be able to make some conjectures where it could apply, but they’d be nothing more than semi-educated guesses.

If I were to hazard a guess, I’d say that any view that takes less time to execute than the time spent by all the middleware on handling that request, and does no (or very little) IO, may take less time in the ASGI environment.

But again, that’s the wildest sort of guess based on what little I actually know about ASGI and I could be 100% off base here.

1 Like

I would love to add another question on to this if possible. Great write up. I have an app, that is fine for most everything BUT 1 or 2 views, were I would like to use SSE streaming responses in async. What is the recommended path for something like this? Switch the entire app to ASGI, or run two instances, merging paths with front end proxy server? I’m trying to find the right fit for it, but not hurt the rest of the app. Thanks in advance.