A website loading is a SELECT somewhere. The user types a URL. The request hits a load balancer. The load balancer forwards to an app server. The app server calls an ORM. The ORM constructs a query. The query goes over the wire to a database server. The database executes it, returns rows. The ORM marshals them into objects. The app server renders a template. The browser receives bytes.
That’s a lot of process boundaries for what is, ultimately, “show this row to that browser.”
I’d been doing variants of that for years. I knew the drill. I just stopped wanting to do it for the small stuff.
The standard pattern
Pick any modern web stack. Rails, Django, Spring, Laravel, Next.js, Phoenix. The shape is the same:
- A web server (Nginx, Caddy) terminates TLS and routes requests
- An application server runs the framework code
- An ORM mediates between the framework’s objects and a database
- A database (Postgres, MySQL) holds durable state
- Often a cache (Redis, Memcached) holds hot copies of database queries
- Sometimes a queue (Sidekiq, Celery) handles asynchronous work
Each layer is excellent at its job. Each comes with its own configuration, its own protocol, its own failure modes, its own deployment story. The stack is robust. It scales. It hires well — every layer has its own community and its own job listings.
This is what you’re supposed to build when you build a web app. Most engineers spend most of their careers inside variations of this shape.
The realization
Most of what I build isn’t Netflix. It’s personal sites, internal tools, a blog, a status page, the occasional small SaaS for myself. The data fits in memory. The traffic fits on one machine. The complexity, before I add the standard stack, fits in my head.
Then I add the standard stack and it stops fitting. Six configuration files become twelve. One process becomes four. One deploy becomes a tier-by-tier coordination problem. A request that wanted to be a function call becomes a network round-trip to a database server colocated on localhost.
There was a moment where I asked the obvious question: why is my database in a different process? It runs on the same machine, the same disk, the same kernel. The split is historical — databases used to be shared across many applications, hardware was expensive, network round-trips were cheaper than process restarts. None of those constraints apply to most of what I build.
I didn’t want to write everything in C — that’s the other end of the trade-off, all control and no safety. I wanted Rust’s safety with a much shorter stack underneath it.
What I tried
Lithair is the result. A single Rust binary that contains the HTTP server, the application logic, and the data store. The “database” is in-memory — declarative models, event-sourced state, persisted via a WAL on the same machine. There’s no second process. There’s no ORM. There’s no localhost:5432.
I built it on Hyper. Rocket and Actix were tempting but pulled in opinions I didn’t want — too much routing magic, too much middleware infrastructure, too much “this is the way to write a web app in Rust.” Hyper is the floor. From there I added what I needed and stopped.
LithairServer::new()
.with_model::<Article>("data/articles", "/api/articles")
.with_frontend_at("/", "public")
.serve()
.await
One process. One deploy. The data lives in the same memory as the request handler. A page load is a function call, not a network round-trip.
Why I prefer this for my projects
My internal blog runs on lithair. The site you’re reading runs on lithair. The framework’s showcase runs on lithair. In each case, the “database” holds almost nothing — server logs in one, articles in another, virtual hosts in a third. The data is small. The traffic is one operator opening a browser.
Putting that in a separate process, with an ORM mapping it to objects, with a connection pool managing connections, with a migration system reconciling schemas — that’s a tax I’d been paying for years without naming it. Lithair stopped paying it.
When the data is small and the traffic is small, monolithic and lightweight is the right shape. Lithair is built for that shape.
What this is not
It’s not “you should write your blog in Rust.” Most blogs are fine in Hugo or Astro or WordPress. The choice depends on what you’re optimizing for.
It’s not “monoliths are back.” Microservices solve real problems at real scale. Just not the problems most personal projects have.
It’s not “you don’t need a database.” You probably do, eventually, for some workloads. Just not for “show me this row” on a low-traffic site.
It’s: when your data fits in RAM and your traffic fits on one box, three tiers is one tier too many. Lithair is what one tier looks like, for me.