Unexpected Egress Charges: Where Bandwidth Bills Come From
Unexpected egress charges usually trace to four habits: serving media from the app, no cache headers, chatty APIs, and cross-region traffic. Find the source and cut the bill without moving providers.
The bill arrives and it is four times what you expected. The compute line is roughly what you budgeted. The line that is not is egress — the bytes leaving your infrastructure — and there is a real chance you have never thought about it once.
Unexpected egress charges are the most common billing surprise in application hosting, and the reason is structural: nothing in your development loop measures bytes. Your laptop does not charge you for them. Staging does not have users. The first time bandwidth becomes visible is on an invoice, and by then you have shipped the habit that caused it.
Here is where it actually comes from, in order of how often it is the answer.
1. Serving media from the application
This one dwarfs the others and it is almost always accidental.
You put a video in /public to demo something. It works, so it stays. Every page view now streams that file out of your container. One 40 MB video on a page with a thousand visitors a month is 40 GB — from a file you thought of as a placeholder.
The same shape applies to product images, PDFs, user uploads, and font files. None of them feel like infrastructure decisions when you add them.
The fix is not clever: static media belongs on object storage behind a CDN, not in your application's filesystem. Your app should serve HTML and JSON. The moment it is serving a 4 MB hero image to every visitor, it is doing a CDN's job with none of a CDN's economics.
2. No cache headers
If your responses have no cache headers, every visitor downloads every asset on every page load. Repeat visitors download it all again. A crawler downloads it dozens of times a day.
# Fingerprinted build assets never change — cache them for a year
Cache-Control: public, max-age=31536000, immutable
# HTML changes — revalidate but allow a short window
Cache-Control: public, max-age=0, must-revalidate
# Anything user-specific
Cache-Control: private, no-store
This is a one-line change per response class that routinely removes most of a bandwidth bill, because the traffic it eliminates is pure repetition.
3. APIs that return more than the client uses
A list endpoint that returns full objects when the UI renders three fields is sending the difference on every request, forever. It never shows up in profiling because it is fast — it is just large.
Two things to check on your busiest endpoints:
- Is compression on?
Content-Encoding: gziporbron JSON typically cuts it by 70–80%. It is a middleware line and it is frequently missing. - Are you sending fields nobody reads? Serialising the whole row because the ORM made it easy is the default behaviour of most codebases.
A polling client makes both worse by a factor of however many times a minute it polls.
4. Cross-region and cross-service traffic
If your app is in one region and your database is in another, every query result crosses a paid boundary. This is easy to do accidentally — you create the database first, pick a region without thinking, and place the app somewhere else weeks later.
Traffic between your own services can also be egress if it leaves the private network and comes back through a public hostname. Two services in the same workspace talking over their public URLs are paying twice for a conversation that never needed to leave.
On Dockup this specific mistake is hard to make, because services address each other by internal name. A service reaches main-db.internal:5432 and another service by its <slug>.internal alias — traffic that stays inside the workspace network never touches a public listener. It is also why the database has no public hostname by default: there is nothing to accidentally route through.
Finding it rather than guessing
Guessing which of the four it is wastes a billing cycle. Measure instead:
Read the access log by bytes, not by count. Sort your top responses by total bytes served rather than by request count. The endpoint sending the most data is rarely the one called most often.
Check your largest static assets. If anything over a megabyte is being served from your app, that is the first thing to move.
# What is the service actually doing right now
dockup metrics my-project/my-api --json
# And what is it logging
dockup logs my-project/my-api -n 1000
Watch for crawlers. A misbehaving bot pulling large files repeatedly can account for most of a month's egress on a site with modest human traffic. robots.txt and a noindex on preview environments are cheap.
The environments nobody counts
Two habits generate egress that never appears in anyone's mental model:
Preview environments. A preview per pull request is genuinely useful and quietly multiplies your footprint. Five open PRs is five copies of your stack, each with its own traffic — often from the same crawlers, because preview URLs get indexed unless you stop them.
Long-lived staging. Staging that has run for eight months, that nobody visits, still pulls images, still gets crawled, still runs its cron jobs.
Neither is a reason to stop using them. Both are a reason to bound them: expire previews when the PR closes, and mark them noindex so search engines stop downloading them on your behalf.
dockup noindex my-project/my-api --on
What good billing looks like
You should be able to answer three questions before the invoice arrives:
- What is currently running? Including the things you forgot.
- What did each one consume? Split into compute and transfer, not one number.
- What happens if it spikes? A cap that stops services is unpleasant. A cap that does not exist is worse.
Dockup meters infrastructure separately from the plan, so the plan fee is not disguising usage — and the Pro plan's $20 credit is applied against that usage rather than being a discount on the fee. The point is that you can see which half is which.
The short version
Move media to object storage. Set cache headers. Turn on compression. Keep services and their databases in one region, and let them talk over the private network rather than the public internet. Expire preview environments and keep them out of search results.
Those five habits address almost every surprise bandwidth bill, and none of them require changing where you host.
Frequently asked questions
What is egress? Data leaving your infrastructure toward the internet. Responses to users, files downloaded, API payloads. Inbound traffic is usually free; outbound usually is not.
Why is my egress bill higher than my compute bill? Almost always because the application is serving files that belong on a CDN. A single large asset on a popular page outweighs weeks of request handling.
Does a CDN actually reduce it? Yes, when it caches. The origin serves the file once per edge instead of once per visitor, and the CDN's per-gigabyte price is typically a fraction of an application platform's.
Do preview environments cost as much as production? They cost what they consume, which is often more than expected because each one is a full copy and preview URLs get crawled unless you prevent it.
