Julia Evans is building what she calls a “2010-style” website: an SQL database, HTML rendered on the server and very little JavaScript.1 Her comfortable toolkit before this was rather different — static site generators, static pages with small JavaScript experiments, or simple Vue SPAs connected to Lambda or a Go backend. That arrangement worked well while an application was basically one page; once she wanted many different pages, the frontend-heavy options became less attractive.1

She is trying to recover on the server what she liked about those small SPAs, namely keeping most of the logic in one place. Django becomes useful for a fairly mundane reason. Evans already knows SQL and backend programming, so she is not waiting for a framework to reveal a new capability; she is discovering how comfortable it is when somebody else has already settled a hundred recurring little jobs.

Small savings

In her July 21, 2026 post, Evans points to modest things — custom QuerySets, template filters, the querystring tag and automatic migrations — that would hardly justify changing stacks one by one, yet together remove a long tail of operations that otherwise get rewritten, renamed from page to page or simply forgotten.1

A few lines from her view show the result. Evans defines an EventQuerySet with approved(), for_tab(), with_festivals(), is_free() and is_outdoors(); when she builds the page, those names make the selections visible immediately, without forcing a reader to decode the underlying columns and operators each time.1

Django explicitly supports this pattern. A custom QuerySet can define its own methods and expose them through a manager; the documentation includes as_manager() and Manager.from_queryset() as ways to make that API available from the model.2

Evans says her earlier instinct was essentially that she already knew SQL, so why bother with a query builder.1 The project changes her mind about a narrower point: local vocabulary such as approved() states the intention immediately, whereas approved_at__isnull=False makes each reader reconstruct it.

Readable query

The difference looks cosmetic while a query fits on one line, then starts organizing the program once a page combines several optional filters.

A Django QuerySet remains lazy, which means it can be built, filtered and passed around before the database is actually touched; evaluation comes later.2 Chaining these helpers does not fire one query per method. Evans dislikes parts of the filter-definition syntax, then spends most of her time on the nicer side of the API: reading the named methods inside ordinary view code.1

Her example separates two questions that ORM arguments often collapse together — whether the syntax is prettier than SQL, and where repeated filtering decisions should live — with the interesting answer mostly belonging to the second.

Ordinary HTML

The same pattern appears in templates through urlize, linebreaksbr, date formatting and json_script. Turning text URLs into links, preserving line breaks, formatting a date or inserting JSON safely is almost comically small work, which is exactly why teams so often end up scattering their own versions of it through a project.1

Their expense comes from attention rather than difficulty: each job is too small to deserve a subsystem and common enough to appear repeatedly, so Django keeps many of them in the standard toolbox of its template engine.3

Evans particularly likes querystring. Her site uses parameters such as ?date=2026-06-01 or ?outdoors=... to represent filters, and the tag can build a link to the same query string with one value changed, or remove a parameter by passing None.1

To be sure, the behaviour still exists somewhere; here it lives in the framework, sparing the application another local helper whose author must check encoding, preservation of unrelated filters and removal of empty parameters.

Nineteen migrations

The same kind of convenience appears below the visible interface, where the project has already accumulated 19 migrations and Evans expects more.1 She does not describe that count as debt; it records how often the schema has been allowed to follow the project while her picture of the problem became clearer.

Django treats migrations as a versioned history of database schema. makemigrations compares current models with the state reconstructed from previous migrations and writes new declarative operations; migrate applies those operations to a database.4 Django's own documentation still recommends reading the generated output because complex changes are not perfectly autodetected.4

QuerySets, templates and migrations all tell the same story: Django industrializes the ordinary case. Difficult migrations still exist, while an ordinary field addition or schema adjustment no longer begins with a procedure invented specifically for this project.

Wrong setting

The reverse side appears when so much useful behaviour arrives preinstalled that part of the system becomes invisible, leaving developers to recognize when they have accidentally switched one of those conventions off.

Evans runs into that problem after LLM-related scrapers begin sending roughly ten requests per second; on a VM costing about $10 per month, a light ApacheBench test then shows the site serving only 2–3 requests per second.1

A CPU profile shows a surprising amount of time spent rendering templates. Django's performance documentation notes that the cached template loader can materially improve performance by avoiding template compilation on every render.5 In Evans' setup the cache was supposed to be enabled by default, but she had accidentally disabled it while changing something else.1

After restoring template caching, she observes around 12 requests per second without exhausting the CPU.1 Evans explicitly warns that this was no rigorous before-and-after benchmark, so the anecdote proves no universal fourfold speed-up; what it does expose is how an invisible convention can perform substantial work until one setting quietly removes it.

Not everything

The post also spends time on conventions Evans dislikes, which keeps it well away from a “ten reasons to use Django” list.

She tries using inheritance to share logic across four class-based views, finds the result unpleasant to follow and returns to straightforward functions.1 Inheriting from a Django interface such as QuerySet bothers her much less, which makes the preference narrower than an anti-class doctrine: the friction appears when her own application logic has to live inside an inheritance hierarchy.

Questions about capacity remain open: whether the site should scale for bursts, whether more work deserves caching, whether Jinja is worth trying, and how much the documented difference between blocks and includes matters in practice. Evans lists them without pretending to have selected a final architecture.1

That uncertainty makes the field report more useful than advocacy, because a framework never removes decisions altogether; it chooses which recurring decisions deserve an already paved path.

One stack

Evans explains that her previous small frontend applications worked well when almost all logic could stay in the browser. For a site with many different pages, she is now chasing the same quality in the other direction: server-rendered HTML, little JavaScript and logic concentrated on the backend.1

Seen through this project, Django looks less like one monolithic framework than a set of continuities between database, URL, view and HTML: migrations carry schema history, QuerySets carry domain vocabulary, templates already manipulate dates and query strings, and shared defaults cover escaping as well as template loading.

In practice, complexity mostly changes address. Evans writes less local plumbing and inherits a broader set of shared conventions, which then need to be familiar enough that an unexpected setting stands out.

Her enthusiasm comes from a developer who already knows how to build these pieces deciding that rebuilding them has stopped being interesting. Go, Flask or a handful of helpers could reproduce the individual behaviours; when the same small jobs recur across thousands of sites, however, letting a framework encounter them first starts to look perfectly rational.