Players Engine

Devlog · Entry 19 · Engineering Notes

The real cost of a social feed

· Nic Vannetti · 5 min read

Every social product lives or dies by its feed, and every feed hides a bill. Before PlayersEngine had any meaningful traffic, I made a rule for myself: no feature ships until I can write down, on one page, what it costs per user per day at a million users. The feed was the first feature to go through that exercise, and it changed how I design almost everything.

The two classic shapes of a feed

There are two textbook ways to build a feed on a document database like Firestore.

Fan-out-on-write means that when someone posts, you copy a reference to that post into a per-follower inbox. Reading the feed is then trivial: one indexed query against your own inbox, already sorted, already filtered. Writes are expensive — a creator with 50,000 followers triggers 50,000 document writes — but reads are as cheap as reads get.

Read-time assembly flips it. Writing a post is one write. Reading the feed means querying the recent posts of everyone you follow and merging them. Writes are cheap; every single feed load pays the price, and it grows with the number of accounts you follow.

Neither is "correct." They're two different loan structures for the same debt, and which one is cheaper depends entirely on your read/write ratio — which, pre-launch, you don't know. So instead of guessing, I put both shapes into a spreadsheet with explicit assumptions: average follows per user, posts per active user per day, feed opens per day, page size. Firestore's pricing is public and brutally simple — you pay per document read, per write, per delete — so once the assumptions are written down, the arithmetic is honest.

What the model told me

The interesting output wasn't a winner. It was a sensitivity: the moment you assume even a modest skew — a small number of accounts followed by very many people — pure fan-out-on-write develops a hot tail. One popular account posting a few times a day dominates your write bill. Meanwhile pure read-time assembly punishes exactly the behaviour you want more of: people opening the app often.

So the answer, unsurprisingly, is a hybrid, and the cost model tells you where to put the seam. Fan out for the common case, where the follower counts are small and the write amplification is bounded. Treat high-follower accounts differently, on the read path, where the extra cost is proportional to actual engaged readers rather than to a follower number that includes people who haven't opened the app in a month. Batching the fan-out writes matters too: Firestore charges per document written either way, but batching cuts the invocation overhead and gives you natural checkpoints for retries.

I won't pretend my exact split is a universal constant — it isn't, and I expect to retune it with real data. The point is that the seam is a dial, and I know which direction to turn it because the model exists.

Why cost beats latency right now

This is the part that feels wrong to most engineers, including me: at this stage, when two designs are both correct, I pick the cheaper one even if it's slower. A feed that assembles in 400ms instead of 150ms is not what kills an early product. A database bill that scales with a quadratic term you didn't notice is also not what kills it — what kills it is that you notice the quadratic term after the growth spike, when the fix requires a migration under load.

Optimizing for cost early is really optimizing for predictability. Indexed, O(1)-shaped reads — "give me this user's inbox page" rather than "scan everything these 800 accounts did lately" — have flat, boring cost curves. Boring curves are the entire goal. Latency you can buy back later with caching and CDNs; a bad cost shape is structural. It's the same reasoning that made scale-to-zero a budget strategy rather than a performance one.

Building the habit: a cost model per feature

The feed exercise turned into a template I now run for every feature that touches the database:

It sounds bureaucratic. In practice it's twenty minutes per feature, and it has vetoed real designs — usually ones where a convenient query hid a scan whose cost scaled with content volume instead of with reader interest.

The takeaway

You cannot know your traffic before launch, but you can know your cost shape. Document databases make every shape explicit if you bother to look: each feature is just a bag of reads and writes with multipliers. Write the bag down. The features that survive that one page of arithmetic are the ones that won't ambush you the month things finally go well — and the discipline is free, which is the best price point a solo founder can get. Where those reads and writes actually live — and why there are two databases under this platform — is the subject of documents and graphs.

Want in on a future wave?

Drop your email and we'll let you know when Players Engine opens up. Early supporters get first access.

← All devlog entries