Skip to content
All writing

Which parts of a social timeline actually cost money

Passion, 9 June 2026

A social timeline looks like one product and bills like twelve. The invoice never arrives itemised by feature. It arrives itemised by unit, and the units have little to do with how the product describes itself. Nobody is billed for "the timeline". You are billed for rows read, bytes stored, images transformed, connection-seconds held open and model calls made. Read the bill that way and it becomes clear which parts are cheap and which are quietly expensive, and the ranking is not the one most people guess.

We built a public microblogging site end to end on serverless infrastructure, partly to have the thing and partly to find where a read-heavy consumer product stops being simple. This is what we learned about the money.

The bill is a list of units

Feature What you are actually billed for How it grows
Reading a feed rows read, plus cache reads page views, blunted heavily by caching
Posting rows written, plus fan-out if you precompute timelines writes multiplied by average follower count
Images stored bytes, plus a charge per variant generated uploads first, then distinct display sizes
Video stored minutes and delivered minutes uploads first, then popularity
Presence and notifications connection duration, per open socket concurrent open tabs, not activity
Link previews one headless browser render per uncached URL distinct links, if you cache by URL
Automated moderation one model call per submitted post or comment writes
Search rows scanned per query corpus size multiplied by query volume
Email one queued message plus one send sign-ups and notification settings

Two things fall out of that table. Most of the features people fret about are cheap. And the two costliest units are not really features: connection duration and headless rendering, both hiding behind small pieces of interface nobody would mention if you asked them to describe a timeline.

Reads are cheaper than they look, if you split the audience

Every feed on our site is readable without an account. That was a product decision, made because a social site demanding a sign-up before it shows you anything can only grow by recommendation from someone who already tolerated that. It turned out to be the best cost decision in the project.

Explore, trending and hashtag feeds are identical for every visitor. One computed result serves everybody, so the cost of the public surface tends towards a constant no matter how many people read it. The home timeline is unique per person and cannot be shared, so it scales with signed-in actives rather than with traffic. That boundary is nearly impossible to retrofit. The moment you personalise a public page, even slightly, even with a well-meant "posts you might like" strip, you convert your cheapest surface into your most expensive one and you do not get it back.

Feed queries run against SQLite at the edge, with optional sharding across four further databases sitting in the configuration, switched off, because nothing has needed it. Sharding is not a cost optimisation. It is a workaround for a size limit, and it makes every query harder to write and reason about.

Duration is the line item that surprises people

A request costs money when it happens. Between requests it costs nothing, which is the whole appeal of the model. A WebSocket costs for as long as it exists, and it exists while someone has left a tab open on a second monitor since Tuesday.

Realtime presence is the purest version of the problem. It bills continuously and produces a green dot. We kept ours because notification delivery needs a live socket anyway, so the marginal cost of presence on top of an already-open connection is small. That is the pattern worth copying: one multiplexed connection per person carrying every realtime concern, rather than one per feature.

If we had to halve the running cost of this application tomorrow, presence would go first and hardly anyone would notice. That is a fair test for anything billed by duration. Ask what happens if it is simply absent, and be suspicious when the answer is "the dot goes grey".

Media is cheap to store and expensive to touch

Storing a photograph costs almost nothing. Doing anything to it costs. Every variant, every resize, every format conversion is a separate charge, so the price of an image is set by how many distinct shapes of it you decide to serve rather than by the image itself.

The cheapest optimisation available is compression in the browser before upload, because one change reduces the upload, the stored bytes and the transform work together. After that, be strict about how many variants a design needs. A thumbnail, a feed size and a full size is three. Designers will ask for seven.

Our upload path sends images to a service that generates variants automatically, and falls back to plain object storage when that service is not configured. The fallback is there for reliability, with a pleasant side effect: the expensive path is optional.

Video is a different category of expense and belongs in its own product decision, not in the attachment picker.

The most expensive single request in the application is a link preview

Turning a pasted URL into a card with a title and a picture means fetching that page and rendering it, which means running a headless browser. It is the heaviest thing the application does, by a wide margin, and a user triggers it by pasting a link into a box.

The whole cost of the feature comes down to one cache key. Cache by URL and a popular link pasted four hundred times costs one render. Cache by post, or forget to cache, and it costs four hundred. The same applies to generated share images: cache on the content that determines the picture, not on the object that contains it.

We paid for a feature we never shipped

Here is the embarrassing one. When a post is created, an embedding is written to a vector index. Nothing reads it. Search on the site is SQL LIKE matching across posts, users and hashtags, and no code path consults that index at all. Every post pays a write towards a semantic search feature that does not exist.

Half-finished features are not free. They cost on the write path, the path you least want to make slower or dearer, and they are invisible, because nothing in the interface hints that they run. Deleting that write is a one line change we should have made the day we decided not to finish the search work.

Automated moderation is the same shape with a better outcome. A model call runs on each new post and comment, and if the binding is absent the check is skipped and the content publishes. That is a cost lever as much as a safety choice, and it is written down as a decision rather than left as an accident.

What we cannot tell you

We cannot give you our bill, because the site has no real user base. The live explore feed returns seeded posts from five demo accounts. We have honest unit counts from the design and no honest traffic to multiply them by, and a cost-per-user figure for an app with no users is a spreadsheet wearing a lab coat.

Estimating before you build

Count units per active user per day, then price them from your provider page:

one ordinary session, per daily active user
   12   feed requests        -> rows read, mostly cacheable if signed out
    1   post with an image   -> 1 write, 1 upload, 3 variants
    3   likes or comments    -> 3 writes plus notification fan-out
   40   minutes of socket    -> 40 minutes of billed connection duration
  0.2   links pasted         -> 0.2 headless renders, on cache miss only

Multiply out and the fourth row is usually the one that changes the answer. Anything charged by duration deserves its own line in the estimate, because it is the only kind of cost that grows while your users do nothing at all.

More on how the site is put together: atHeartbeat.