Skip to content

Why we ship one binary and SQLite

· 2 min read · Renoxium

Most of the time a web page spends before it reaches you isn’t spent computing anything. It’s spent waiting: for a connection to a database on another machine, for a cache on a third, for a framework to wake up. Every network hop is a few milliseconds that nobody chose on purpose.

So we start from the other end. Each Renoxium app is one program, and its database is a file next to it.

One program

Renox apps compile into a single binary. rnx build puts the pages, the translations, the CSS and the migrations inside it, so a release is one file:

rnx build            # dist/renoxium: the whole app
scp dist/renoxium server:/srv/renoxium/
ssh server systemctl restart renoxium

There’s no runtime to install on the server, no node_modules, no container image to rebuild for a typo. The restart is seamless too: rnx make:deploy writes a systemd socket that holds incoming connections while the new version starts.

The database is a file

SQLite runs inside the app’s process. A query is a function call, not a round trip over the network. Renox opens it with the settings a web app wants:

PRAGMA journal_mode = WAL;   -- readers never wait for the writer
PRAGMA synchronous = NORMAL; -- safe in WAL mode, far fewer fsyncs

In WAL mode many requests read at the same time while one writes, which is exactly the shape of most web apps: lots of reading, a little writing.

But does it scale?

Further than most products ever need. A single modern server with an SSD handles a lot of traffic, and an app that answers in a millisecond or two needs far less hardware than one that waits on the network. When an app does outgrow one machine, Renox speaks PostgreSQL too: the same models and queries, a different DATABASE_URL.

Backups that run

A database that’s a file is easy to lose, so it’s backed up continuously: rnx make:deploy sets up Litestream, which streams every change to object storage. Restoring is one command.

What we gave up

  • Many app servers behind a load balancer. One server, scaled up, until that stops being enough.
  • Choosing a database per feature. One database, used well.

In return there are fewer things to monitor, fewer things to break at 3 a.m., and pages that don’t wait on anything they don’t need. That trade is what the rest of this series is about.

  1. · 3 min read Deploys that refuse no connections Restarting a single-binary app without dropping requests: systemd socket activation, a graceful shutdown, migrations both versions accept, and continuous SQLite backups.
  2. · 3 min read Catching N+1 queries before they ship Renox has no lazy relations on purpose. What it has instead: loaders that fetch a page's related rows in one query each, a debug page that flags repeated SQL, and tests that count queries.

Get new posts by Atom, RSS or JSON Feed.