Notes · Daedalus

Forty-three thousand rooms and no server

What it actually takes to serve a music knowledge graph as flat files, and the four places the approach hurts.

26 August 2026 · 1,386 words · Siegfried Martens

43,567 rooms turn out to be 111,728 files and 1.4 GB on disk. Every one is written once by a Python script and then never computed again.

The cheapest part is the hosting. The expensive parts are a four hour cache that silently hides deploys, and a content delivery network that blocks you from checking your own work.

One constraint I carried as a launch blocker through two write-ups did not exist. A single look at the account settled it: 56,000 files used of a one million limit.

Orpheus is a site where you can walk around a music knowledge graph. Every artist has a room, every genre has a room, and the rooms link to each other by who played with whom and what came out of what. There are 43,567 of them.

There is no backend. Nothing runs when you click: every room was written once, by a Python script on my desk, into a file that sits on a disk and is handed to you unchanged.

I want to describe that honestly, because “static site” is usually said as a boast and the interesting part is the bill.

What is on the disk

The room count is the number I quote, and it undersells what is on the disk by two and a half times.

what is actually on the disk artist rooms 40,673 842 MB section sidecars 38,502 392 MB discography sidecars 29,607 130 MB style rooms 2,894 73 MB assets, index, page 52 3.2 MB 111,728 files 1.4 GB
43,567 rooms, 111,728 files. A room is not one file. The page you land on is small and loads fast; the heavy parts, the section text and the discography, are separate files (the sidecars in the chart) fetched only if you open them. That split is the performance strategy, and it is why the file count is what it is.

The search index is a single 4.1 MB file. It is the only thing every visitor downloads in full, and everything else is fetched on demand.

What it buys

The list is short.

Nothing can go down except the disk. There is no process to crash, no connection pool to exhaust, no memory leak, no dependency to patch on a Tuesday. The classical web vulnerabilities do not apply: there is no query to inject into and no session to steal, because there is no query and no session.

It is also fast in the only way that matters, which is that the file is already sitting next to the person asking for it.

What it costs to run

The domain is $10 to $15 a year. The hosting is a shared plan I already had. That is the entire recurring bill, for a graph of 43,567 entities.

That is not a trick. It is what you get when you decide in advance that nothing will be computed at the moment somebody asks, and the four places below are the price.

Where it hurts

1 · A change can be correct on the server and invisible to everyone

This is the one that cost me the most and would be the easiest to miss.

The page loads its main script with a timestamp attached, so that script is never served stale. What I had not thought through is that the main script then imports the other modules by relative name, eleven of them at the time, and a relative name does not carry the timestamp. Those eleven were being cached for four hours by the delivery network that sits in front of the site, which section 2 is about.

Since I split the frontend into modules, most of the behavior lives in those eleven files. So “the main script busts its own cache” was protecting the one file that had almost nothing in it.

I found it on a deploy that appeared to do nothing. The main script was serving fresh while one module came back from the cache with an age of 6,090 seconds and the previous version in it. The new code was correct on the server and unreachable by visitors, and nothing errored, because the stale module exported exactly the same names as the new one. It was simply the old behavior, quietly.

A deploy that is silently a no-op for four hours is a specific price for having no server. A server would have shipped the new code the moment it restarted.

2 · The protection blocks you too

The site sits behind a content delivery network, largely because static files are trivially enumerable: the search index is a complete list of everything that exists, so anybody who wants the whole corpus can fetch that once and then loop. There is no server, so there is nowhere to put a rate limit. The network in front is the only place that logic can live.

The consequence is that scripted requests get challenged, which is the point, and that includes mine. I cannot verify a deployed data file with a command line fetch, because my own protection returns a challenge page instead of the file. To check what is actually deployed I have to connect to the origin machine directly, going around the front, or open a real browser and look.

So verifying my own work is a manual act. When the only place you can enforce a policy is also the only path to your data, the policy applies to you too.

3 · A data change is a deploy of the whole corpus

There is no database to update, so nothing can be changed in place. A change to how rooms are generated does not take effect anywhere until every affected file is rewritten and copied up.

The last structural change rewrote 38,512 files and pushed 147 MB over the wire, compressed about 2.7 to 1. The room files also sit behind the same delivery network as everything else, with a cache life of about a week, so a data deploy is not visible until that cache is cleared as well. None of that is slow, but it is a different mental model from changing a template. Every derived value, every formatted date, every computed label, is frozen into tens of thousands of files at the moment the script runs, and correcting one of them means running the script again.

4 · You will fear a limit you have not measured

I carried a launch blocker for months: the file count was going to hit the hosting account’s inode limit, the number of files an account may hold. I wrote it down twice, once in my design notes and once in a write-up, as a known constraint.

It was never true. When I finally opened the account in May and read the numbers, it was using 56,000 files of a one million limit and 1.3 GB of a 976 GB quota. When I looked again in August, before the expansion that added 38,500 files, the account was at 2.8 GB of a 20 GB quota and the quota page no longer listed a file limit at all; the expansion took it to about 172,000 files. The constraint I had been designing around, and writing down twice, did not exist and one look would have retired it at any point.

I include it because it is the truest thing here about what this architecture costs: the numbers that look scary in a static build are file counts, and checking one takes a minute.

Where the approach ends

It does not end at scale: 111,728 files is not close to anything, and the same design would survive several times the corpus.

It ends where a question has to be answered rather than looked up. Everything above works because every possible page is knowable in advance, so all of it can be computed once. The moment somebody asks something in their own words, or the answer depends on who is asking or what happened five minutes ago, there is nothing to precompute and the model breaks completely.

That is exactly what happened. Pythia, the question-answering service that runs beside Orpheus, is real code on a real machine, because it has to be. It runs beside the static site rather than replacing it: 43,567 pages you can walk around, served as files, and one small process for the thing files cannot do.

What I would take from this

  1. “No server” is a real answer for a read-only corpus, and it is much cheaper and much more robust than it sounds. The bill is a domain name.
  2. Price it in deploy friction. Every cost above landed at deploy time, and none of it on an invoice: a cache that hides changes, a shield that blocks verification, a rewrite of tens of thousands of files.
  3. Cache busting is not transitive. If your entry point busts its own cache and imports anything by relative path, those imports are stale and your deploy is a no-op you cannot see.
  4. Measure the ceiling before you architect around it. I designed against an inode limit for months and was at 5.6% of it.

Orpheus is at orpheus.rocks. Pick an artist you like and follow the links out of their room until you get somewhere you did not expect.

Next in the series: The ruler came first

Siegfried Martens, 26 August 2026. Part of the Daedalus notes: what was built, what it measured, and what the measurement could not see. Questions and corrections to daedalus@s-martens.com.

If your data has a labyrinth in it, let’s talk.

I take a small number of engagements, the interesting kind. The fastest way to find out whether yours is one of them is a conversation.