43,567 rooms are stored as 111,728 files and 1.4 GB on disk. Every one is written once by an offline Python script and then never computed again.
The hosting is cheap. Pain points were more subtle, like Cloudflare’s four hour cache that silently hides deploys, and a content delivery network that complicates checking your own work.
Orpheus is a first-person exploration of a music knowledge graph, as if you were wandering in a maze. Every room shows you information about an artist or a musical style, and rooms are linked to each other by many properties such as related-artist or inspired-by. There are 43,567 of them.
There is no backend DB or compute. Nothing runs when you click: every room was written once, by a Python script on my desk, and is handed to you unchanged.
What is on the disk
The topline room count represents less than half of the full file count.
A single 4.1 MB file, the search index, is the only thing every visitor downloads in full; everything else is fetched on demand.
What it buys
With no server, nothing can go down except the disk. There is no process to crash, no connection pool to exhaust, no memory leak, no broken dependencies to patch. There is no query to inject into and no session to steal, because there is no query and no session.
What it costs to run
The domain name costs $10-$15 a year. The hosting is a shared plan I already had. That’s the entire recurring bill to host an immersive graph of 43,567 entities.
This isn’t a trick; it’s just a benefit of hosting a graph defined from precomputed assets, and it’s not without costs.
Where it hurts
1 · A change can be correct on the server and still be invisible
Gotchas resulting from caching cost me time chasing down red herrings.
The main script is timestamped, so it never gets served stale. However, I hadn’t considered that the main script then imports the other modules by relative path, and a relative name does not carry the timestamp. Those referenced modules 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 modules. So timestamping my main script was protecting the one file that had almost nothing in it.
I found it because of a deploy that appeared to do nothing. The main script was serving fresh but a sub-module was being served stale. 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. The old behavior was being served, quietly.
A deploy that is silently serving a stale version for four hours is one price for having no server.
2 · The CDN’s anti-scraper protection blocks you too
The site sits behind a Cloudflare content delivery network (CDN), because static files are trivial to download: the search index lists every file in the entire corpus, so anybody who wants to just download everything can fetch that once and then loop. With no server, that’s hard to prevent, since there is nowhere to put a rate limit. The CDN in front is the only place that logic can live.
What that meant for me, though, is that I couldn’t automate tests of the deployed work. Scripted requests got challenged (which was the point), including my own. I couldn’t easily check from the command line that a data file had deployed, because my CDN’s protection returned a challenge page instead of the file. To check what had actually deployed I had to connect to the origin machine directly, or manually open a browser and look.
For the most part then, verifying my own work was done manually. I needed a policy restricting bots from my data, but the policy applied to me too.
3 · Any real data change is a redeploy of the whole corpus
With no database to update, nothing can be changed in place. A change to how rooms are generated isn’t visible until every affected file is rewritten and copied up. A lot of the rooms are densely connected, so small changes can have big impacts.
The last structural change rewrote 38,512 files and pushed 147 MB over the wire. The room files also sit behind the same CDN 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 · Don’t fear limits you haven’t actually measured
I’d been concerned for months because I’d misremembered my hosting account’s inode limit, i.e., the number of allowed files. I’d been trying to keep my file counts from growing too much because I was afraid I would hit that limit.
I shouldn’t have worried. When I finally took the time to reread the details in May, I was only using 56,000 files of a one million limit and 1.3 GB of a 976 GB quota. More recently, before an expansion that added 38,500 files, the tally 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. I’d wasted time designing around a non-existent constraint; one look would have retired it at any point.
Because it’s a static build, the file counts were a lot more salient than if everything had been packaged up in a database, which is the only reason they almost became an unnecessary blocker.
Where the approach stops scaling
111,728 files isn’t close to using up my file hosting quota; the approach would survive several times the corpus, and the plan is to keep the corpus growing automatically.
The limitations of this approach start to be felt more where a question has to be answered rather than looked up. Everything above works because every possible page is knowable in advance, so it can be precomputed. On the other hand, if you want to ask a question about the data, or the answer depends on who is asking or your earlier path through the corpus, the model breaks completely; you can’t precompute an answer under these circumstances.
This is what drove me to build Pythia, named for the original oracle. Pythia is a question-answering service, and it does run code on a server, because it has to. It doesn’t replace the static site; it runs alongside it. Orpheus gives Pythia a foundation of 43,567 pages to ground its answers, and in return, Pythia gives Orpheus a conversation front-end.
What I would take from this
- For the right read-only corpus, you don’t need a server, and this yields a much cheaper and more robust system than you would think. The bill is a domain name.
- The price was deploy friction. My gotchas were mostly at deploy time, and none of them cost me money: the cache that was hiding changes, the anti-bot shield blocking testing, the global impacts of small changes to the graph.
- Cache busting by timestamps is not transitive. If your entry point uses cache busting but then imports anything by relative path, those imports are stale and your deploy can fail silently.
- Measure the ceiling before you architect around it. I designed against an inode limit for months and was at 5.6% of it when I eventually checked.
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.