We're seeing some capacity limits, so parts of the site may be slow or briefly unavailable while we work on an upgrade. Please try again later, or join our Discord for updates.
Discord
Dev blog
Behind the scenes

The cheapest byte is the one you never send

Dota Captain
7 min read

The story of a bandwidth and compute audit, where the biggest wins came not from clever code but from simply not sending bytes I never needed to send. Download lazily, cache aggressively, and treat polling as a backup.

The number that stopped me was not a spike in traffic. It was a line on a usage graph for data transfer, quietly climbing in a way it had never done before. When I started Dota Captain I did not think about bandwidth, because at a few visitors a day it costs nothing, and a few wasted megabytes here or there rounds down to zero. Then tens of thousands of people showed up in the first month, which I still find slightly hard to believe, and every small inefficiency I had been ignoring suddenly had a very large multiplier attached to it. This is the story of the audit I did into where the bytes and the compute were actually going, and the not-very-glamorous fixes that followed. The lesson lives in the title. The cheapest byte is the one you never send.

When a rounding error becomes a bill

For most of this project's life, the running cost of serving the site was effectively zero, and I treated it that way. I never measured how much each visit downloaded, because it did not matter. If a page pulled down an extra megabyte it never played, that was a problem for a version of the site I did not have and did not expect to have.

Then the site got popular faster than I planned for. Tens of thousands of visitors in a month is a wonderful thing to type out, and I am genuinely grateful people showed up at all. But popularity has an arithmetic to it. Waste that is invisible at ten loads becomes a real number at thirty thousand. Data transfer and server compute stopped being a rounding error and became a line item, and that forced me to go looking at what I was actually shipping to people.

I want to be honest that this is me learning the operations side on the fly, on a free tier, after the fact. None of what follows is clever. It is mostly me finding things I should never have been doing in the first place, and stopping doing them.

Five megabytes of audio nobody hears

The biggest single bleed was audio, and it was almost funny once I saw it. The draft screen was eagerly downloading all of its ambient tracks on every load, a laning loop plus both win songs, about 5 MB in total. The catch is that only one of those tracks ever plays in a given session. You hear the laning loop, or you hear the win song for whichever side wins. You never hear all of them. So I was paying to deliver roughly 5 MB to every visitor to guarantee they would experience maybe a third of it.

The fix was almost insultingly small. I changed the audio preload hint from auto to none, so the browser only downloads the track you actually hear when it is time to hear it. That one change stopped the site from pushing music at people who might close the tab before a single note played.

While I was in there I re-encoded the four ambient tracks to a leaner 80 kbps mono, which took them from about 5.2 MB down to about 3.3 MB with no difference anyone is likely to notice in a background loop. I also dropped an eager sound-effects preload of about 300 KB per load, because those clips already load lazily on first play, so preloading them was pure duplicated waste.

Cache what never changes, and remember to rename it when it does

The second lever was images and animations. A lot of the visual assets on the site are fixed. They do not change from one visit to the next, so there is no reason a returning visitor's browser should download them again. I set a one-year immutable cache on images and animations, the same treatment the audio already had, which tells the browser it can reuse its saved copy for a very long time instead of asking the server for the file again.

This is one of the highest-leverage things a small site can do, because the cheapest possible request is the one the browser satisfies from its own disk without ever contacting me. For a returning reader, a huge chunk of the page is simply free.

There is a trap in immutable caching, and I want to name it because it is exactly the kind of thing that bites you months later. If a file is cached for a year and you then change its contents while keeping the same filename, everyone who already has the old copy keeps seeing it, potentially for up to a year. The fix is a discipline, not a feature. When you change a file, you rename it, so every browser treats it as brand new. I wrote that rule down in the project notes so future me does not quietly ship a change that half the audience never sees.

Polling should be a backup, not a heartbeat

Bandwidth was only half the audit. The other half was compute, and the culprit there was redundant polling. The PvP waiting room had a backup poll that ran on a steady heartbeat, asking the server for updates over and over. That is fine as a safety net, except it was running even when the real-time connection was perfectly healthy, and it kept running even when the browser tab was hidden and nobody was looking at it.

So I gated it properly. The backup poll now only runs when real-time updates are actually unhealthy and the tab is genuinely visible. I removed a duplicate poll that was doing the same job twice, relaxed another one's interval so it fires less often, and made all of them stop entirely while the tab is in the background. A hidden tab needs nothing from me, and it was quietly asking anyway.

The shift in thinking is the useful part. Polling on a fixed heartbeat treats the server as something you must keep prodding. Treating it as a backup means you only prod when the thing you were relying on has actually failed and someone is actually there to care. Most of the time that is never, and never is a lovely amount of work to do.

A few smaller cuts in the same spirit

A handful of other fixes followed the same principle of not doing work I did not need to do. Instead of running a live scan of a hundred rows every time a signed-in user visited a lobby, I served a shared intel rollup cached for sixty seconds, so one computed answer covers everyone who arrives in that window rather than each visit paying for its own scan.

I also debounced a version check that had been firing on every focus and every visibility change, which meant tabbing back and forth was generating requests for no good reason, and I removed an always-on prefetch of a page that could never be cached anyway, so the prefetch was buying nothing and costing something.

None of these are individually dramatic. Each one is a small amount of waste I had left lying around when the multiplier was tiny. Adding them up is the whole point, because small per-load waste times tens of thousands of loads is a real bill.

The lesson, stated plainly

If there is one thing to take from this, it is the title. The cheapest byte is the one you never send, and the cheapest computation is the one you never run. When you have a handful of users you can afford to be sloppy, and honestly you should be, because your time is better spent building the thing than shaving milligrams off it. But the moment real numbers of people show up, sloppiness stops being free.

The three rules that came out of my audit are simple enough that a non-engineer can hold them. Download only what this session actually needs, instead of everything it might need. Cache what never changes so returning visitors get it for free, and rename it when it does change so nobody gets stuck on a stale copy. And treat polling as a backup for when real-time breaks, not a heartbeat that runs no matter what.

I got some of this wrong in ways that inconvenienced real people before I fixed it, and I am still figuring out the operations side as I go. But the audit paid for itself many times over, and the best part is that most of the fixes made the site faster for everyone at the same time as making it cheaper for me. Not sending a byte is good for both of us.

Behind the scenes