Why a free app can't just buy a bigger server
By jagduvi
Tags: behind the scenes, performance, free, open source
In short: Every Cellarion feature is free for everyone, forever. That promise has a consequence: when more people arrive, we can't just pay for a bigger server the way a paid app can. So we measured where Cellarion's servers spend their time and money, and made it lighter. Photos now take about a tenth of the space, each AI label scan costs about a third of what it did, and smart-home integrations cost the database a quarter of what they did.
The shortcut a paid app has
When a paid app gets slow, there's an easy answer: a bigger server. More processors, more memory, more machines. It costs more, but every new customer brings a payment with them, so the bill grows with the income. Performance becomes a line on an invoice.
Cellarion doesn't work like that. Every feature is free for everyone, and the people who support us do so voluntarily. When more people arrive, our costs grow and our income doesn't have to. For free to keep working as Cellarion grows, it has to get cheaper to run for each person, not just bigger.
And some problems a bigger server doesn't solve at all. A list that stops at a fixed length, a step that makes everyone queue behind it, or an AI call that's priced per use behaves the same on the largest server money can buy. Those take changes in the code, whatever the budget.
Was the old code bad?
No, and it's worth saying why. It was written for the app we had: correct, readable and fast enough. At today's size the server spends most of its time idle. Most of what we changed was a choice that's right for a small app and wrong for a big one:
- working out your statistics from scratch every time something asks,
- keeping photos in a format that never loses a pixel,
- sending a bottle's whole record when a list only shows a few things about it,
- letting a tab in the background keep checking for news.
That's the normal order of things: build what people need, measure, then make it lighter where the measurements point. Tuning code before you know where the time goes mostly tunes the wrong code. Measuring also turned up a few plain bugs along the way, and those are fixed too.
How we looked
We started from the live server's own numbers, a day and a half of them: which requests arrive, how often, from what (browsers, search engines, smart-home integrations), and what each one costs the database, the processor and the memory. We read the code behind the busiest paths. Then we asked what would break first at ten, a hundred and a thousand times today's active users.
Each fix was built on its own, tested on a separate copy of the whole system, measured before and after, and released on its own, so each one could be checked by itself.
Doing less work for the same answer
- Smart-home integrations. Integrations like Home Assistant check in every few minutes, day and night, and every check worked out that person's statistics from scratch. Together they were the biggest single load on the database. Now an answer that hasn't changed comes straight from memory: 3 database reads per check instead of 12.
- The rack view. Showing a rack sent the full record of every bottle in it, down to the long descriptions of each region and grape: 4.9 MB for a 700-bottle cellar. It now sends what the page shows, 1.35 MB, and loads about three times faster. The rack map on the cellar page needs just 37 KB.
- Open browser tabs (in the next update). A tab left open asked the server about 386 times in a day, mostly while nobody was looking at it. The announcement banner checked every five minutes all night, and every switch back to the browser reloaded the whole notification list. Now hidden tabs stay quiet, and the bell asks a quick "anything new?" instead of fetching the list. The same day: 49 requests.
- The activity log. Integrations were logged every hour they were used, and those entries had grown to 41% of the whole log. Once a day tells us the same thing.
Smaller things to store and send
- Photos. Bottle photos were stored in a lossless format, about 430 KB each on average. They're now WebP, at about a tenth of the size and a quality where the difference is very hard to see. We converted every existing photo as well, and photo storage shrank by more than 90%. Removing the background from a large phone photo now takes about a third of the time and 40% less memory, because the photo is scaled down to 2,048 pixels first.
- Searching your own cellar. Your bottles used to be copied into a separate search engine, private notes included, and that copy would have outgrown the server's memory at about twice today's users. Search in your own cellar now runs in the database itself. Before switching, we compared the two on more than 24,000 searches: the same bottles every time, in the same order 99.93% of the time. The few differences were odd searches like "pino tnoir". It's at least as fast, and there's one fewer copy of your notes.
The expensive part: AI
The biggest bill isn't the servers at all. Every label scan is a paid call to an AI model, and at every size we looked at, AI costs more than all the servers put together. Most of what a scan cost was the model reading the same long instructions again each time. We now have the AI provider keep those instructions cached between scans, so each scan costs about 65% less, and in our tests it read labels just as accurately. Nothing changed about how you add a bottle.
Ten, a hundred, a thousand times
Here's roughly what the numbers say about growing. These are estimates from today's measurements, not promises:
- 10× today's users: the current setup holds, with one step up in server size.
- 100×: money alone doesn't get there. Parts of Cellarion do all their work in a single process on a single machine, and they'll have to be split up. That's code, not hardware.
- 1,000×: several machines, with photos in separate storage. At that size, the servers would cost roughly five times as much without this kind of work as with it, and be far more fragile.
We'll be doing this again. And again.
Removing one bottleneck doesn't make a system fast for good. It shows you the next one. That doesn't mean something went wrong; it's how growing software works. So this isn't a one-off clean-up. As Cellarion grows we'll measure again, fix what the numbers point at, and write about it here. Next on the list: the offline copy of your cellar, which is currently rebuilt from scratch after any change, how the web server handles connections, and how much code your browser has to download again after each update. That work is what lets Cellarion stay free as it grows.
All of this is live on cellarion.app from version 1.236; the open-tab change arrives with the next update. As always, every change is in the open repository, so self-hosted copies get lighter too. Questions or ideas? The forum is open.
Nobody should have to pay for what they need. Some people like to pay for what they love. Skål!