Skip to content

I Estimated 200 KB Per User. That Killed the Sharding Plan.

An afternoon of table-by-table arithmetic sized the disk, set an enforced write limit, and took distributed databases off the roadmap for a million users.

My fitness app is a machine for creating rows. Training plans, exercises, meal plans, food items, shopping carts, shopping lists. All of it sits behind a REST API on purpose, because I want people automating their own workflows against it.

Which means the documented happy path is also the attack. Anybody with an access token and twenty minutes can write a while true loop that creates training plans. A hundred. A million. Until something stops it. On the day I first thought about this properly, nothing did.

Nobody steals data or escalates privileges here. They use the API exactly as documented, faster and longer than a human ever would, and my Postgres disk is a fixed number of gigabytes that only moves one direction. The cost to attack is a for loop. The cost to me is every write in the product failing at once, on whatever random Tuesday the counter crosses the line.

So before launch I did the boring thing. I went table by table and estimated what one real user weighs. The answer was about 200 KB, and that one number paid out twice. It told me where to put a wall. It also told me I don’t need sharding, not at a million users, not at any scale I have business planning for.

what one user actually weighs

You go through it table by table. A training plan row is a name, a few foreign keys, some timestamps. Small. Hundreds of bytes. The exercises attached to it are mostly references to a catalog I already store once, so each link is tiny. Nutrition plans are similar. The items inside them are the chunkiest part because there are more of them, but they’re still short rows. Shopping carts and shopping lists are derived, mostly pointers.

Now think about how a person uses this. Somebody serious about training has maybe a handful of plans. They rotate a few. They’re not curating four hundred distinct mesocycles, because they’d have to do them. Nutrition is the same shape. A few plans, a repeating set of foods, because humans eat roughly the same twenty things.

Put a generous multiplier on all of it and you land in the low hundreds of kilobytes. I called it 200 KB.

The multiplier is the important part, and it’s where a napkin estimate earns its keep. I wasn’t trying to be right. I was trying to be wrong in a direction I could afford, so I rounded every row up, assumed more plans than anyone keeps, and never once tried to shave the number down. An estimate you’ve deliberately inflated is still useful when it turns out low, because you know which way it failed.

sharding is a response to a number

Here’s the multiplication that settled it. A million users is 200 GB, which you can rent without a meeting. Ten million is 2 TB, which is a bigger machine and still one machine.

One relational database holds all of it. Which means no distributed transactions, no routing layer, no resharding project for some future engineer to dread, and no class of bug where a user’s data is split across two nodes and one of them is having a bad day. All of that is work I am now not doing, and the reason I’m not doing it is a multiplication I could have run on my phone.

This is the part I’d push back on if someone told me sharding was on their roadmap. Which number is it answering? If you haven’t got one, you’re not planning an architecture, you’re copying one from a company whose constraints you’ve never checked against your own.

The estimate didn’t just size a disk. It took an entire category of complexity off the table, and it took an afternoon.

the limit goes where nobody can reach it

Here’s the part I think people get wrong, because it sounds like a product decision and it’s really a safety one.

I turned 200 KB into an enforced ceiling. Not a soft warning. A refused write.

The instinct is to be nervous about that. Limits annoy people. Limits generate support tickets. Limits are the thing you read about on Reddit when some app decides you can only have twenty of something and you needed twenty-five.

But that only happens when the limit sits near real behavior. Mine doesn’t. It sits far above anything a person using the app as an app would produce. The gap between what a committed user creates and where the wall is isn’t a few percent. It’s orders of magnitude. Which means the population that ever touches the wall is, as far as I can figure, exactly the population I’m trying to stop.

That’s the design goal, and it’s worth stating plainly because it resolves the tension people feel here. A good quota is invisible to everyone acting in good faith and immovable to everyone who isn’t. You don’t get that by picking a number that feels safe. You get it by estimating real behavior first and then setting the wall well past it, deliberately, with room you never expect to use.

Get it wrong in the other direction and you’ll know, because your support inbox tells you. That feedback loop works. There’s no equivalent loop that tells you your limit is too generous until the disk is full, and a full disk is not a normal outage. A normal bug hits one code path. This one hits everything with a write in it, at the same time, and the application stays read-only until I’m awake, provisioning storage, and figuring out whose rows to delete.

I should be honest about what I’m accepting here, though, because a refused write is a blunt instrument. Somewhere out there is a legitimate power user with a genuinely enormous training history, and if they hit my ceiling they get an error, not a conversation. I haven’t built an appeal path. I took that trade deliberately: the worst case on one side is a single annoyed person I can raise a limit for manually, and on the other side it’s every write in the product failing at once. Those aren’t comparable, so the decision is easy. It’s still a real cost, and pretending a quota has no victims is how you end up defending a number long after it stopped matching reality.

the number has a shelf life

The obvious objection to all of this is that 200 KB is a number about today’s schema, and schemas grow.

It’s a fair hit. Add progress photos tomorrow and the per-user weight doesn’t drift, it jumps by orders of magnitude, and every conclusion I drew from the estimate expires the day that feature ships. The estimate isn’t a fact about my app. It’s a fact about my app’s current tables. That makes it the same kind of written-down assumption as everything else in the design, and it gets re-run when the schema changes, not when the disk starts filling.

The enforced ceiling helps here in a way I didn’t plan. A limit that nobody legitimate ever reaches is also a tripwire. If real users start hitting a wall I set orders of magnitude above real behavior, that’s not a support problem, it’s the schema telling me the old arithmetic is stale. A disk that quietly fills up over eighteen months tells me nothing until it tells me everything at once.

do the math before they do

Whatever you’re about to launch, or already launched and never looked at this way, it’s a short exercise.

Write down every place a user can create something. Not features, creation points. Every endpoint that turns into a row, an object, a file, a job. If there’s an API, assume every one of them will be called in a loop, because eventually one of them will be. Estimate what one reasonable user produces across all of them, multiply by the user count you’d like to have, and check that against what your storage actually holds. Then set the ceiling far above the reasonable number and enforce it at the write.

You’ve seen this exercise before, probably in a system design interview, where back-of-the-envelope estimation was step two right after clarifying requirements. It’s less fashionable now, and fairly so, because in a real job you’d open a dashboard instead of guessing. But a dashboard only tells you what is already happening. The estimate tells you what to build before there’s anything to measure, which is exactly when the architecture decisions get made.

That’s what the afternoon bought. One number that paid out twice, once as a wall at the write that stops the loop, once as a distributed database I never had to build. Most teams reach for the second one without ever computing the first.

Do the math before they do.

Peace, nerds.

DSGVO Cookie Consent mit Real Cookie Banner