Serving Static Files with uWSGI

As noted in a previous post, uWSGI is a Swiss Army knife of functionality. One of its features is a built-in static file server.

When to Use uWSGI for Serving Static Files

While it may not be as efficient as something like Nginx, don't rule out using it. We've found a couple of scenarios where it shines:

  1. Smaller sites where running a single service keeps things simple, e.g. Docker or Heroku.
  2. Any site that has a reverse proxy (a.k.a. origin pull) CDN doing the bulk of the static file serving.

In both scenarios, serving static files is not a resource intensive task. Bringing in an additional service to just to handle it would be more complexity than necessary.

The Simple Case

For most sites, this is all that's needed in your uWSGI configuration file:

static-map = /static=/path/to/static/files
static-expires = /* 7776000
offload-threads = %k

Let's break that down:

static-map = /static=/path/to/static/files

Serve a request to /static/style.css from /path/to/static/files/style.css.

static-expires = /* 7776000

Set the Expires header so the client (or CDN) will cache it for 90 days.

offload-threads = %k

Spawn a number of offload threads equal to the number of CPU cores to handle these requests. This prevents tying up a worker that would otherwise serve your app. The offload threads are lightweight and according to the docs "can manage thousands of file transfers concurrently."

A Problem with the Simple Case

static-map will fallback to the application if the file requested is not found on the filesystem. At first glance, this seems innocuous, but we've seen it cause major issues in the past.

Consider a scenario where you have multiple app servers with a rolling deployment where, for a short window, some servers have new versions of the code while others still run the previous version. During that window, it's possible for one app server to reference a static file that does not yet exist on another server.

If this happens, Django will field the response and try to append a slash to it by issuing a 301 Moved Permanently, e.g. /static/style.css gets redirected to /static/style.css/. If that 301 is then cached at the CDN, you have a real problem. None of your users will be able to view the file until both the CDN and their local browser cache expire!

The proper solution to this problem would be to make sure that the correct file is always served, perhaps via a shared filesystem or sticky sessions in your load balancer. In a pinch, however, we can prevent uWSGI from falling back to the app server and serve the expected 404 Not Found instead. Additionally we'll add a Cache-Control header to tell the CDN to avoid caching the 404. This isn't a perfect fix (some requests could 404 within the deployment window) but, in some cases, that's good enough.

The new configuration uses a goto 😱 and looks like this:

static-expires = /* 7776000
offload-threads = %k

# If the path starts with /static/ go to the route-label "static"
route-if = startswith:${PATH_INFO};/static/ goto:static

route-label = static
# Remove /static/ from ${PATH_INFO} to find the file on the filesystem
route = ^/static/(.*) rewrite:$1
# If the file exists, serve it
route-if = isfile:/path/to/static/files/${PATH_INFO} static:/path/to/static/files/${PATH_INFO}
# If the file is not found, serve a 404
route-run = addheader:Cache-Control: no-cache
route = .* return:404
# Close the "static" route label
route = .* last:

Compared to something like Nginx, uWSGI's routing configuration is pretty archaic, but it is surprisingly powerful and gets the job done. It gets complex quickly, so try to avoid throwing too much logic in there.

If you have any other uWSGI tips or settings you'd like to learn about, drop a note in the comments below.