django-planet
April 8, 2023

Django Build To Last

in blog Screaming At My Screen
original entry Django Build To Last

Tim does an amazing job applying "Build to Last" principles to Django. Community and reliability are two of the three most important reasons making me enjoy Django for a very long time (documentation being the third). I think it is healthy for any project to know what its distinguishing attributes are, as well as having a plan for where the project will go the next few years. And while I agree with Tim, I want to add a bit more to the big hairy audacious list of goals to take into consideration.

Still, this does not mean deployment is not one of the biggest pain points of any Django or Python project. I would even go as far as claiming that Docker would never have risen in popularity as much as it did if interpreted languages would have had a sane deployment story. Remember Java? Compile a fat jar and drop it on an application server. Done. Or just run the fat jar by bundling your webserver. But this is the old man in me speaking. So let us talk about Golang and Rust - two modern languages, well regarded (I know, I know, we can have a fight about which one is better later) and both gaining traction. Guess what the deployment story is - compile, drop on a server and call it a day. Or if your deployment infrastructure requires it wrap the executable in the smallest possible container.

But this post is only partly about deploying Django applications. I still want to acknowledgement that it is - in my humble opinion - one of the most important things to work on. What we are about to talk about ties into deployment complexity of the whole stack.

It seems like Nuitka might be a solution to the problem. I did not have a chance to test it, but if the documentation holds up to its promises we might be close. I also appreciate that they have a plan to monetize features likely to be only relevant to businesses while the open source version is pretty much feature complete for what most people would need.

Deploying an ecommerce application

Let us take a high level look at your average deployment of an ecommerce project. While details will vary, there is a good chance you will see most of the components in one form or another.

  1. a webserver, loadbalancer or reverse proxy
  2. the actual Django application
  3. a database of some sort
  4. a key value store
  5. the actual Django application, but this time as background worker

A customer of mine with an engineering team of two built their ecommerce website (30-50k customers / day) using Golang.

  1. a webserver, loadbalancer or reverse proxy
  2. the actual Golang application
  3. a database of some sort

You might ask what is the least we could get away and still handle the traffic if closer to 100% uptime would not be required - downtime during a server reboot would be considered acceptable for example.

  1. the actual Golang application and SQLite

They all serve the same purpose, just at different scale and using different technology. I am mostly focusing on small to medium sized projects here. "But your example does not make sense if you want to have Instagram scale." No it does not. Nearly no tooling will help you with that out of the box. But most projects are not and never will be at this scale.

Most ecommerce solutions want to:

  • display products
  • let people put things in a shopping cart
  • charge credit cards
  • send confirmation mails
  • expose orders to a back office

As you can see complexity of the deployment changes once you move to Golang. This is from my experience not to attribute to Golang simply having a higher throughput.

Sending emails

One thing we have to do is send a confirmation mail. We obviously do not want to do this during the regular request / response cycle.

In Django land we define a task which we put on a queue so a background worker can pick the task up and do its job.

In the Golang world we usually run go send_mail(context). While at a certain scale you absolutely want a queue as well, not necessarily for a few ten thousands requests a day as long as you are smart about your retry logic and still keep track of the tasks in case of crashes. Realistically at some point you would save the mail task and have a coroutine started during application startup pick it up.

// app start
go send_mail(datastore)

// view
queue_mail(contenxt)

The same applies to large inventory operations. Pre-warming caches. Charging credit cards. And much more.

There are so many things we need background workers for. With background workers comes the need for some form of queueing solution which often means a key value store. All of this because the async capabilities of Python and Django are not where they could be by now. Things are surely looking better. async is finding its place. But it is in my opinion under-utilised and would help especially at smaller scale. Would it eliminate the need for a key / value store or task queue? No. But it would likely delay the point where you need it.

We are already seeing different solutions like stator which is part of the takahē project. A more elegant solution than my current goto which is a cronjob running a management command. Essentially web app and worker using the DB as queue. Which is fine and a lot more scalable than most people think. But it still requires two processes to run. stator is pretty close to the Golang example likely being "good enough" for most projects.

Big hairy audacious goal (BHAG)

Long story short - I believe Django needs to get a way better handle on asynchronous code before it can hope to figure out its deployment story.

I had the questionable pleasure of reading most of Celerys code. I contributed a small patch to Django-q. I am currently deploying Huey at a decent scale. All of these, no matter what I personally think of the code quality, served me well for a long time and solve an important problem.

But none of these are close to what something like stator packaged in contrib would be and having a consumer / worker process ran during webserver startup.

Even better if it would allow for a pluggable queue for the moment the project outscales this approach. There is nothing inherently wrong with the Django stack I described above. It is the one we all know will scale well and is flexible to work with. But it should not be assumed to be the standard you reach for when just publishing a project for 30 users a day.

Reaching the same level of concurrency support as Golang has would require significant changes in Python itself. I would categorize these as not very likely to happen any time soon considering the load of problems and changes this would bring with it.

But what we can do on a framework level is adding a little bit of multiprocessing, a standardised way to move jobs from the view or business layer to a worker and doing it in a way that supports any scale from proof of concept to "what was the maximum number of messages in SQS again?".

Solving asynchronous execution alone would in my opinion improve the deployment story a lot. Pairing it with Nuikta (testing pending) would in my opinion nearly solve all the deployment problems, issues and pain points I personally experienced often enough and I regularly see people mention.

I am surely curious what the future will bring and where Django will go. And I am even more curious when I look back at this post in five or ten years how far off I was with my understanding what the BHAG is and what the best approach to tackle it would be.