The 1,500 Dollar Day: The Egress Bill That Reframed Our Whole Strategy

About 5,000 users watched video in a day and CloudFront egress cost us 1,500 USD. The GB-per-view math, why egress is the silent killer, and the fix.

The 1,500 Dollar Day: The Egress Bill That Reframed Our Whole Strategy
Contents

When I moved transcoding to AWS MediaConvert, I wrote — almost as a footnote — that I had traded a compute problem for a bill I now had to watch. The compute problem was solved: uploads encoded in parallel, the queue stopped climbing, my Celery workers went back to doing app work. The bill was the part I had waved at and moved past. This chapter is that footnote coming to collect.

The morning I remember most from the Happy Thoughts platform wasn’t a launch or an outage. It was a billing alert. On an ordinary day, roughly 5,000 users watched our videos, and CloudFront quietly served them about 1,500 USD worth of bandwidth in 24 hours. Nothing had broken. That was the terrifying part — the system worked perfectly, and that was the problem.

For a venture-backed startup, a 1,500 USD day of usage-driven cost is a rounding error you celebrate. For a non-profit running a devotional-education platform on donations and modest course fees, it’s a five-alarm fire. Because the alert wasn’t telling me something was wrong. It was telling me something was right — people loved the content, they were watching a lot of it — and that every unit of that love now had a price tag attached that grew the more we succeeded.

First, what egress even is

One term does all the damage in this chapter, so let me define it in plain words before it costs us anything. Egress is data transfer out — bytes leaving your infrastructure and travelling across the internet to a user’s device. When a phone in Pune presses play, a video file has to physically move from a server, through a CDN (a content delivery network — a fleet of servers spread around the world that keep copies of your files close to users so playback starts fast), out to that phone. Every one of those bytes is billed. Storing a file costs you a flat monthly rent. Serving it — egress — costs you every single time someone watches, and it is metered by the gigabyte.

That distinction is the entire chapter. Storage is a fixed cost you can see on a shelf. Egress is a variable cost that fires on every play button, and it is the one almost nobody models until it lands.

Where the bytes were hiding: Cost Explorer, grouped by usage type

The billing alert gave me a scary total but no story. AWS bills are a single blended number until you make them confess. The tool that makes them confess is Cost Explorer — AWS’s cost-analysis console — and the trick is to stop looking at services and start looking at usage types. A usage type is the granular, region-prefixed SKU AWS actually meters against: not “CloudFront,” but the specific line like IN-DataTransfer-Out-Bytes — bytes transferred out from CloudFront’s India (IN) edge locations to viewers.

I pulled it straight from the API, grouped by USAGE_TYPE, for the day that scared me:

aws ce get-cost-and-usage \
  --time-period Start=2024-02-11,End=2024-02-12 \
  --granularity DAILY \
  --metrics UnblendedCost UsageQuantity \
  --filter '{"Dimensions":{"Key":"SERVICE","Values":["Amazon CloudFront"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE

The result made the whole invoice legible in one glance:

USAGE_TYPE                     UsageQuantity        UnblendedCost
IN-DataTransfer-Out-Bytes       8,982.44 GB          $1,527.01
IN-Requests-HTTPS-Proxy         1,806,400            $     1.99
US-DataTransfer-Out-Bytes         214.10 GB          $    18.20
AP-DataTransfer-Out-Bytes          61.77 GB          $     6.24

There was no ambiguity left. One usage type — IN-DataTransfer-Out-Bytes — was 98% of the CloudFront bill. Not requests (2 million HTTPS proxy requests cost all of two dollars). Not storage. Not the US or Singapore edges. Egress to Indian phones, metered by the gigabyte. That single line is the entire chapter, and Cost Explorer’s usage-type grouping is the one report I now open before I open anything else on a media bill.

The bill, reconstructed

Here’s the arithmetic behind that one line, and it’s worth walking through slowly because the shape of it is the whole lesson.

Our course videos were about 20 minutes long. We had deliberately not built an adaptive-bitrate ladder for these older files yet (a decision I’ll defend in a second), so each shipped as a single high-bitrate HD file — around 1.2 GB. No lower renditions for a 4G phone on a train. One fat file, served whole, to whoever pressed play.

Do the multiplication:

  • ~5,000 users, averaging ~1.5 videos each that day → roughly 7,500 views
  • 7,500 views × ~1.2 GB per view → roughly 9,000 GB, i.e. ~9 TB egressed in a day

And here’s the detail most people building on AWS from outside the US miss: CloudFront egress is priced by the destination region, and India is one of the expensive ones. At the time we were paying about 0.17 USD per GB for the first 10 TB/month to India edge locations — nearly double the ~0.085 USD/GB you’d pay serving the same bytes in the US or Europe. So:

8,982 GB × $0.17/GB ≈ $1,527

There’s a knob here that looks like a fix and isn’t. CloudFront price classes let you restrict which edges serve your content: PriceClass_100 (US, Canada, Europe only), PriceClass_200 (adds India, Middle East, Africa, most of Asia), and PriceClass_All (everything). Dropping to PriceClass_100 would take our Indian traffic off the pricey India edges and serve it from cheaper US/EU ones — the per-GB rate falls, the bill drops. But it drops because the bytes now travel from Virginia or Frankfurt to a phone in Pune instead of from a Mumbai edge 30 ms away. You buy a lower rate by paying it in latency, on exactly the slow mobile connections this whole platform exists to serve well. That’s not an optimization; it’s moving the cost from the invoice to the user. The rate was high because the edges were close, and close was the point.

The whole chain, from a happy audience to a painful number, is just four multiplications stacked on top of each other:

There it was. Not a bug, not an attack, not a misconfiguration. Just our own audience, watching the content we’d asked them to watch, on the CDN we’d told to serve it.

Why egress is the silent killer

Every other cost in a cloud architecture announces itself. Compute shows up when you provision an instance. Storage shows up as a slowly climbing line you can see coming. Egress is different. Egress is the cost that:

  • Doesn’t appear in load tests. You test correctness and latency on a staging environment nobody’s paying bandwidth to watch. The bill only exists in production, under real traffic.
  • Doesn’t page you. There’s no 500 error, no p99 spike, no failed health check. The system is healthiest exactly when egress is highest.
  • Scales with success, not with failure. Every architectural instinct we train — retries, caching, graceful degradation — is about surviving things going wrong. Egress punishes things going right.
  • Arrives a month late. By the time the invoice lands, you’ve already served the bytes. You can’t un-send them. You’re doing forensics on a decision you made 30 days ago.

That combination — invisible in testing, silent in monitoring, linear with usage, delayed in billing — is why I now call egress the silent killer of media platforms. It’s the one cost that scales precisely with the metric your product team is celebrating in standup.

The GB-per-view math is the entire game

Sit with the per-view number, because it’s the lever that controls everything downstream.

A 1.2 GB file over 20 minutes is a constant bitrate of about 8 Mbps (1.2 GB × 8 bits / 1,200 seconds). That’s a heavy, desktop-grade HD stream being pushed, unmodified, to phones on metered Indian mobile-data packs. It was also the source of a completely separate user complaint we already knew about: a single 20-minute video could eat a user’s entire daily data pack. The egress bill and the user pain were the same root cause wearing two different costumes.

Now imagine that video transcoded properly into an adaptive HLS ladder, where a phone on a weak connection pulls a 720p rendition at ~1.5 Mbps:

20 min × 1.5 Mbps = ~225 MB per view

That’s roughly a 5x reduction in bytes served — from 1.2 GB to ~225 MB — for a viewing experience that, on a small phone screen, most users literally cannot tell apart. The same day, the same traffic, two completely different bills:

Applied to that 1,500 USD day, transcoding alone would have taken the bill toward ~300 USD, while simultaneously fixing the data-pack complaint. One change, two problems, same fix.

That’s what I mean when I say GB-per-view is the entire game. Everything else — which CDN, which storage backend, which region — is a coefficient. GB-per-view is the exponent.

The cache-hit-ratio trap, and why HLS makes it worse

My first instinct on seeing the bill was the wrong one, and it’s worth showing why, because it’s the mistake everyone makes. I thought: my cache hit ratio must be bad — fix that and the bill drops. Cache hit ratio is the fraction of requests the CDN serves from its own edge copy without going back to your origin storage. A 95% hit ratio means only 5% of requests fall through to S3.

Here’s the trap. Egress to the viewer is billed at the edge, on every single request, hit or miss. When a Mumbai edge streams a segment to a phone, those bytes leave CloudFront and get metered as IN-DataTransfer-Out-Bytes whether the edge had the segment cached or had to fetch it first. Cache hit ratio does nothing to that $1,527. You cannot cache your way out of viewer egress — the viewer is downstream of the cache, always.

What hit ratio does control is the second, quieter bill: origin fetches. Every cache miss makes the edge pull the object from S3 before it can serve it. And this is exactly where HLS turns a manageable problem into a punishing one. A single flat MP4 is one cacheable object. The same lesson as an HLS ladder is a manifest plus hundreds of tiny ~6-second .ts segments, across three or four renditions — one “video” fanned out into thousands of distinct small objects:

20 min lesson at 6 s segments  = 200 segments per rendition
4 renditions (360p/480p/720p/1080p) + manifests
                                ≈ 800+ cacheable objects per title
~800 objects × full catalogue   = the working set blows past edge cache

Do the request math on the miss side. At ~200 segment requests per view (the rendition a given phone actually pulls) and 7,500 views, that’s ~1.5 million edge requests in a day. The origin-fetch volume is requests × (1 − hit_ratio):

hit ratio 0.95 → 1,500,000 × 0.05 =    75,000 origin GETs/day
hit ratio 0.60 → 1,500,000 × 0.40 =   600,000 origin GETs/day

An 8x swing in origin load from a 35-point drop in hit ratio — and HLS’s thousands-of-small-objects footprint is precisely what drives hit ratio down, because a long-tail devotional catalogue can’t all stay hot in every edge’s cache. The S3 GET charges are pennies ($0.0004/1,000 requests), but each cold miss adds an origin round-trip before the segment can stream, which is felt as playback stall on the exact slow connections we care about most. So HLS is a double-edged sword: it slashes GB-per-view (the bill that matters), while multiplying object count and stressing cache (the latency that matters). The lesson I internalized: caching is a latency and origin-load tool, not an egress-cost tool. The only way to cut viewer egress is to send fewer bytes or send them from a network that doesn’t meter them — which is where the next two chapters go.

The COGS trap that makes this existential for an NGO

Here’s the part that turned a bandwidth-optimization problem into a strategy problem in my head. COGS — cost of goods sold — is simply what it costs you to deliver one unit of the thing you sell. For a video platform, the dominant unit of COGS is egress: the bytes you push per view.

In a normal SaaS, you sanity-check unit economics — the profit or loss on a single user — with one question: does revenue per user comfortably exceed cost-to-serve per user? If a paying user brings in 500 rupees a month and costs you 50 to serve, you have margin, and you can spend it on growth. The gap is your business.

On Happy Thoughts, the numbers were structurally hostile to that framing:

  • A large share of users paid nothing — the mission is to make spiritual education accessible, so free access is the point, not a funnel stage.
  • The users who engaged most — who watched the most videos — generated the most cost, and engagement did not correlate with payment. Our most devoted users were often our most expensive users.
  • “Revenue per user” was, for the median user, effectively zero, while “cost to serve per user” grew every time we shipped better content that people actually watched.

In a donation-funded org, cost-to-serve isn’t a line item you optimize after product-market fit. It is the constraint that determines whether the mission is financially survivable at scale at all. Put bluntly: for a mission-driven organization, unit economics aren’t a spreadsheet exercise — they are the strategy. If serving one more grateful user costs more than it brings in, then “growth” is just a faster path to running out of a donor’s money. Success, unpriced, is indistinguishable from failure.

In defense of having deferred transcoding

It would be easy to read this and conclude I’d made a rookie mistake — how do you run a video platform with 1.2 GB files still on the wire? So let me defend the earlier decision honestly, because the sequencing was deliberate.

Months before the 1,500 USD day, we consciously chose not to invest in transcoding the older catalogue. At that point we were still validating whether anyone would pay for these courses at all. A proper ladder is real engineering — an encoding pipeline, storage for every rendition, playback that negotiates bitrate. Building all of that to serve videos nobody had yet proven they’d pay to watch would have been the textbook mistake of optimizing a thing before you’ve earned the right to. The correct move at that stage was to ship the fat files, watch the numbers, and learn.

The 1,500 USD day wasn’t the deferral failing. It was the deferral succeeding — it was the exact signal we’d been waiting for. Demand was now undeniable, which meant the deferred debt had come due and, critically, we’d finally earned the right to pay it down. Deferring the work was right. Forgetting to instrument the cost of deferring it was the actual mistake.

Model it before it models you

The concrete change I’d have made from day one costs almost nothing: put a single number on the dashboard, right next to daily active users, that estimates what those users cost to serve.

def projected_daily_egress_cost(
    daily_active_users: int,
    views_per_user: float,
    gb_per_view: float,
    usd_per_gb: float,   # region-specific; ~0.17 for India CloudFront then
) -> float:
    gb_served = daily_active_users * views_per_user * gb_per_view
    return round(gb_served * usd_per_gb, 2)

# The 1,500-dollar day, seen a month early:
projected_daily_egress_cost(5_000, 1.5, 1.2, 0.17)   # -> 1530.0

# The same day, if we'd transcoded first:
projected_daily_egress_cost(5_000, 1.5, 0.225, 0.17) # -> 286.88

Ten lines. Had that number been on a wall chart, the 1,500 USD day would have arrived as a boring, expected point on a graph I’d been watching climb for weeks — not as a billing alert that ruined a morning. The failure wasn’t the cost. The failure was being surprised by a cost that was, in hindsight, trivially predictable from metrics we already had.

This reframed how I steer every media-heavy system now. Alongside DAU, retention, and conversion, I keep a cost-to-serve-per-active-user number, and I treat egress as a first-class architectural constraint — the same way I treat latency or correctness — from the first design doc, not the first invoice. Every product metric gets a COGS twin sitting right beside it.

The transferable lesson

If you take one thing from this chapter, take this: model egress and cost-of-goods-sold as first-class constraints from day one, not as a finance problem you’ll get to after you’ve scaled.

For any product where the cost to serve a user rises with how much they use it — video, large downloads, LLM tokens, high-resolution media — your unit economics don’t reward you at scale, they test you at scale. Every efficiency win (GB-per-view, tokens-per-request, bytes-per-session) compounds in your favor, and every deferred efficiency compounds against you the instant you succeed.

And if you’re building for a mission-driven organization, this isn’t a nice-to-have discipline. It’s the whole thing. A non-profit doesn’t get to out-raise its inefficiency the way a VC-backed company can. The unit economics are the strategy — because the only version of the mission that survives is the one that can afford its own success.

The map that pointed at the next two chapters

Once I accepted that viewer egress was the immovable object, the question narrowed to one number: what does a gigabyte to an Indian phone cost on someone else’s network? I priced the same 8,982 GB/day against every delivery model I could realistically move to. The spread is not a percentage — it’s an order of magnitude, and it’s the reason this series takes the turns it does next.

Delivery pathEgress model~Per-GB to IndiaSame 8,982 GB/day
CloudFront (S3 origin)Metered, region-priced~$0.17~$1,527
Bunny.net (storage + CDN)Metered, cheap volume tier~$0.01~$90
Cloudflare CDNBandwidth not billed$0~$0
Wasabi + CloudflareBandwidth Alliance, no egress fees~$0~$0

Two things jump out. First, Bunny.net alone — an S3-style object store with a CDN priced for the developing world — turns a $1,527 day into a ~$90 one without changing a single byte we serve. Second, and more radically, Cloudflare doesn’t meter bandwidth at all on its standard model, and Wasabi is a member of the Bandwidth Alliance — a pact where partner clouds waive egress fees between each other. Wasabi’s object storage rents at a flat ~$6.99/TB-month with no per-GB egress charge, and Cloudflare pulls from it and fans out to viewers for free. Pair them and the egress line on the invoice doesn’t shrink — it disappears.

That’s the exit. Not a cheaper CloudFront, not a better cache ratio, but a different physics: storage priced like a shelf and delivery priced at zero. The catch — the one that turns the next chapter from a victory lap into a saga — is that Wasabi is S3-compatible, not S3, and the transcoding pipeline I’d just so carefully rebuilt on MediaConvert flatly refuses to write anywhere that isn’t real AWS. Freeing the egress bill meant breaking the encoder. But that’s the next chapter’s fight.

The 1,500 USD day didn’t just send me hunting for a cheaper CDN. It permanently changed the first question I ask of any system that serves bytes to people: what does it cost when this works? Answering it meant going hunting for the one thing that could make our own success stop feeling like a threat — delivery so close to free that watching more no longer cost more. That hunt is the next chapter.