Last year I sat in a quarterly finance review where our CDN bill had jumped 34% in a single month. Finance wanted to know which product team was responsible, and every team pointed at each other — video said it was the new image optimization rollout, web said it was video's 4K push. We didn't have an answer because we'd never built proper showback or chargeback. If you're seeing that same finger-pointing, you're not alone, and it's exactly why CDN cost attribution matters before the bill gets painful.
Showback vs. Chargeback: Why You Don't Start by Charging Teams
I've watched a lot of companies try to jump straight to chargeback — actually debiting business units for their CDN usage — and it almost always backfires. Teams get defensive, they start arguing about the math, and engineering spends weeks debating allocation instead of fixing costs. Showback is the gentler, smarter first step. You measure and report what each team *would* have been charged, but no money actually moves. It gives everyone visibility without the political fallout, and that visibility is what changes behavior.
When we rolled out showback at a previous media company, we didn't even try to be perfect on day one. We started with simple hostname-based attribution, shared a monthly report, and let teams see their numbers for two quarters before we ever talked about real chargeback. By the time we flipped the switch, there were no surprises. Teams had already optimized their own cache hit ratios because they could finally see that their origin fetches were driving 40% of the egress bill. If you want chargeback to stick, you’ve got to earn trust with showback first.
The mechanics are straightforward, but you need consistent tagging from the edge. We tag every request at the CDN with a business unit identifier — usually via a custom response header or an edge worker that maps hostnames, URL paths, and even header values to an owner. That metadata then flows into your logs and becomes the foundation for both showback and later cost allocation. Don't rely on billing reports from your vendor alone; they won't know that video.example.com and api.example.com belong to different P&Ls. You have to enrich the data yourself, as described in the HTTP Working Group Specifications for handling custom headers and cache behavior.
-- Simplified showback attribution from CDN logs in BigQuery
-- Assumes you've added x-business-unit at the edge via VCL / Worker
WITH enriched_logs AS (
SELECT
timestamp,
request_host,
uri_path,
edge_pop,
bytes_sent,
cache_status, -- HIT, MISS, PASS
-- Fallback mapping if header is missing
COALESCE(
http_headers['x-business-unit'],
CASE
WHEN request_host LIKE 'video.%' THEN 'video-platform'
WHEN uri_path LIKE '/api/%' THEN 'platform-api'
ELSE 'web-marketing'
END
) AS business_unit,
-- Egress cost isn't flat - apply regional rate
CASE edge_pop_region
WHEN 'NA' THEN 0.045
WHEN 'EU' THEN 0.065
WHEN 'APAC' THEN 0.09
END AS rate_per_gb
FROM cdn_access_logs
WHERE timestamp BETWEEN '2024-03-01' AND '2024-03-31'
)
SELECT
business_unit,
COUNT(*) AS total_requests,
SUM(bytes_sent) / POW(1024,3) AS egress_gb,
SUM(bytes_sent) / POW(1024,3) * ANY_VALUE(rate_per_gb) AS attributed_cost_usd,
AVG(IF(cache_status = 'HIT', 1, 0)) AS cache_hit_ratio
FROM enriched_logs
GROUP BY business_unit
ORDER BY attributed_cost_usd DESC;
Designing a Cost Attribution Model That Finance Will Actually Trust
A good CDN cost attribution model isn't just about bytes. Your bill is a mix of egress bandwidth, requests, cache invalidations, and sometimes compute at the edge, and each component needs a different allocation key. I've seen teams allocate everything by bandwidth and then wonder why their API team — with billions of tiny, uncacheable POSTs — looks artificially cheap. You have to break the bill into its real drivers. For most providers, CDN Egress Cost Modeling: Understanding and Reducing Your Largest Bill Component is still 70-80% of the total, but CDN Request Pricing Analysis: Reducing Per-Request Costs at Scale can dominate for API-heavy workloads, while invalidations and shield fetches hit differently.
We usually split costs into three buckets. First, egress and bandwidth, allocated by bytes_sent per business unit weighted by region — because a gigabyte out of Sydney isn't priced like one out of Virginia, especially if you're comparing CDN Bandwidth Commitment Pricing: When Committed Plans Save Money vs Pay-As-You-Go. Second, request costs, allocated by raw request count. Third, shared platform costs like minimum commits, support fees, or unused committed capacity, allocated by percentage of total usage or negotiated ownership. If you don't handle that third bucket, your showback will never reconcile to the actual invoice, and finance will lose confidence immediately.
Cache efficiency has to be part of the model too. It feels unfair to charge a team for origin shield fetches caused by another team's low TTLs, but that's what happens with naive allocation. We now track origin egress separately and attribute it to the team that caused the MISS. That one change made our web team bump their default TTL from 1 hour to 24 hours after they saw their true origin cost. Guidance from web.dev Articles on caching best practices helped us set sensible defaults before we enforced them with costs.
# Python: monthly chargeback calculation with shared cost allocation
import pandas as pd
# Load enriched usage from logs (GB and requests per BU)
usage = pd.read_csv('march_attribution.csv') # columns: business_unit, egress_gb, requests, origin_gb
# Real rates from your contract
RATES = {'egress_per_gb': 0.055, 'per_10k_requests': 0.0075, 'origin_per_gb': 0.08}
# 1. Direct attribution
usage['egress_cost'] = usage['egress_gb'] * RATES['egress_per_gb']
usage['request_cost'] = (usage['requests'] / 10000) * RATES['per_10k_requests']
usage['origin_cost'] = usage['origin_gb'] * RATES['origin_per_gb']
usage['direct_cost'] = usage['egress_cost'] + usage['request_cost'] + usage['origin_cost']
# 2. Allocate shared platform fees (e.g., $15k commit shortfall + support)
shared_fees = 15000
total_direct = usage['direct_cost'].sum()
usage['shared_allocation'] = (usage['direct_cost'] / total_direct) * shared_fees
# 3. Final chargeback amount
usage['chargeback_total'] = usage['direct_cost'] + usage['shared_allocation']
print(usage[['business_unit', 'direct_cost', 'shared_allocation', 'chargeback_total']].round(2))
# Reconcile: usage['chargeback_total'].sum() must == vendor invoice total
Putting It Together: An Example Allocation Report Finance Can Use
You don't need a perfect data warehouse to get started. In our first iteration, the entire report was a single scheduled query and a spreadsheet finance could audit. What mattered was that every line item tied back to the vendor invoice total — no rounding gaps, no "unallocated" bucket larger than 2-3%. If your model leaves 15% of the bill unallocated, teams will dismiss the whole thing as guesswork. We set a rule: if we can't attribute it, we label it explicitly as "shared platform" and allocate it transparently, rather than hiding it.
Timing and granularity matter as well. Don't wait for month-end to show teams their burn. We push a daily showback dashboard so teams can spot spikes early — like when marketing launched unoptimized 4MB hero images and tripled their egress in 48 hours. Catching that during the campaign, not 30 days later, saved us over $18k in overage that quarter. That feedback loop is the real value of showback; chargeback is just the enforcement mechanism later on.
Below is a simplified version of what our monthly showback table looks like after reconciliation. We keep it in terms finance understands — cost, not just terabytes — and we always include the cache hit ratio and origin percentage so teams see *why* their cost is what it is. When a business unit owns its drivers, it starts making better engineering choices without you having to force it.
| Business Unit | Allocation Key | Egress (TB) | Cache Hit Ratio | Monthly Attributed Cost |
|---|---|---|---|---|
| Video Platform | Hostname: video.* + /hls/ path | 182.4 | 92.3% | $11,840 |
| E-Commerce Web | Hostname: www.* / shop.* | 94.7 | 78.1% | $7,210 |
| Platform API (Mobile) | Header: x-bu=api + /api/ path | 21.3 | 14.5% | $5,950 |
| Marketing Sites | Hostname: blog.* / campaigns.* | 45.2 | 84.6% | $3,430 |
| Shared Platform Fees | Pro-rata on direct cost | - | - | $15,000 |
Automating Showback: Building a Daily Allocation Pipeline That Doesn't Break
Here's where most teams stall — they've got a beautiful cost model in a spreadsheet, but it's updated manually once a quarter and nobody trusts it. If you want finance to actually use this, you've got to automate it daily and make it auditable. We've found the sweet spot is a nightly job that pulls three sources: your CDN's usage API (bandwidth, requests, and edge compute executions), origin logs or your log delivery bucket for cache status and byte attribution, and your CMDB or tagging service that maps hostnames and URL patterns to business units.
Don't try to be perfect on day one. Start with 95% attribution and dump the rest into a "Shared Platform" bucket you review weekly. The key is tagging discipline at the edge. Every request needs a deterministic owner — usually via hostname (video.yourcorp.com vs. docs.yourcorp.com) or a path prefix (/api/billing/*). We enforce this with a worker at the edge that stamps an x-bu-id header and logs it. Without that, you'll spend months arguing about whether that 40 TB spike was Marketing's campaign or Product's new auto-play feature. And trust us, finance won't accept "we think it was Marketing" when you're allocating $18k in overage.
Your pipeline also needs to handle the stuff that doesn't show up in simple bandwidth reports — like CDN Storage Cost Optimization: Tiering, Compression, and Lifecycle Policies for origin shield and edge persistence, or CDN Edge Compute Cost Optimization: Function Pricing Models and Efficiency Tricks if you're running Cloudflare Workers or Lambda@Edge. That 2.3 million daily image transforms at $0.60 per million doesn't look like much until you annualize it to $500+. Allocate that separately by execution count, not bandwidth. Here's a simplified version of the allocator we run in production:
# nightly_cdn_allocator.py - simplified cost attribution logic
import pandas as pd
# 1. Load daily usage: expects hostname, cache_status, bytes, requests, bu_id
logs = pd.read_parquet("s3://cdn-logs/dt=2025-10-14/")
rates = {"bandwidth_per_gb": 0.085, "request_per_10k": 0.0075, "shield_per_gb": 0.02}
# 2. Tag validation - anything untagged goes to Shared for manual review
logs['bu_id'] = logs['bu_id'].fillna('shared_platform')
untagged_pct = (logs['bu_id'] == 'shared_platform').mean() * 100
assert untagged_pct < 5, f"Too much untagged traffic: {untagged_pct:.1f}%"
# 3. Calculate attributed costs per BU
logs['gb'] = logs['bytes'] / (1024**3)
logs['bandwidth_cost'] = logs['gb'] * rates['bandwidth_per_gb']
logs['request_cost'] = (logs['requests'] / 10000) * rates['request_per_10k']
# Only charge origin shield / midgress on MISS + PASS
logs['shield_cost'] = logs.apply(
lambda r: r['gb'] * rates['shield_per_gb'] if r['cache_status'] in ['MISS', 'PASS'] else 0, axis=1
)
allocated = logs.groupby('bu_id')[['bandwidth_cost', 'request_cost', 'shield_cost']].sum()
allocated['total'] = allocated.sum(axis=1)
allocated.to_csv("s3://finops-showback/cdn_allocation_2025-10-14.csv")
print(allocated.sort_values('total', ascending=False).head())
When Attribution Gets Messy: Shared Cache, Origin Shield, and Reserved Commitments
If your allocation report is always clean, you're probably not looking hard enough. The three things that break simple models are shared assets, midgress/shield costs, and how you handle commitments. Take shared assets first: that common React bundle at /static/app.v2.js might be requested by three different business units but cached once at the edge with a 96% hit ratio. You shouldn't charge the BU that happened to warm the cache for the full origin fetch while giving the others a free ride. We solve this by splitting MISS costs proportionally to HIT volume over a 7-day window. If BU-A drove 70% of the hits for a shared object, it absorbs 70% of that MISS cost. It's not perfect, but it's defensible.
Midgress and origin shield are the next gotcha. That second layer of caching between your PoPs and origin can cut origin egress by 60-80%, but the shield PoP charges aren't free — typically $0.02-$0.04 per GB. Don't bury that in platform overhead. Attribute it back to the BUs with the lowest cache hit ratios, because they're the ones causing it. We've seen a single low-cache API (hit ratio 18%) generate $3,400/month in shield fees alone, while the high-cache video team (94% hit ratio) generated almost zero. Finance needs to see that causality.
Finally, don't allocate the list price if you've bought reserved capacity. If you've locked in 100 TB/month at $0.03/GB via CDN Reserved Capacity Pricing: Locking In Rates for Predictable Workloads, but your blended list price is $0.085/GB, you've got a ~65% discount to pass along. Allocate the *effective* rate, not list. We calculate a daily effective rate = (commit fee + overage cost) / total GB delivered, then apply it uniformly. Keep the commitment tracking separate so you don't accidentally incentivize a BU to think bandwidth is more expensive than it is — that just leads to weird local caching hacks that hurt overall performance.
Frequently Asked Questions
How do we fairly allocate costs for a cache HIT that was originally fetched by another BU?
Don't attribute the MISS to whoever fetched it first. Instead, aggregate shared objects (like common libraries or fonts) weekly and split their total origin fetch costs — bandwidth plus shield — proportionally by each BU's HIT count for that object. You'll need to maintain a small lookup table of shared paths or content hashes. For the first 30 days, just tag the top 50 shared assets; that'll cover 80%+ of the shared volume.
Should we allocate based on billable bandwidth or origin offload?
Always allocate on billable edge bandwidth and requests, since that's what the invoice says. But show "origin offload savings" as a secondary column — e.g., "BU-A: $4,200 billed, $9,800 saved via 78% hit ratio." That turns the conversation from "you spent this" to "your caching strategy saved this," which is critical for showback adoption. We've seen BUs improve hit ratios by 12 points in one quarter just from seeing that savings number.
When should we move from showback to actual chargeback?
Don't flip the switch until you've had at least two full quarters of showback with variance under 5% and sign-off from each BU lead. Chargeback without trust just creates shadow IT. A good trigger is when a BU starts asking, "Can we pay less if we improve caching?" — that's when you know they believe the numbers. Start chargeback with bandwidth only (60-70% of the total bill), keep compute and shield as showback for another quarter, then expand.
How do we handle burst overages and 95th percentile billing vs. flat per-GB?
Normalize everything to an effective per-GB rate for allocation, even if your contract is 95/5. Calculate your total monthly CDN cost / total billable GB and use that as your allocation rate. Trying to allocate percentile bursts to the BU that spiked at 2am on a Tuesday creates endless arguments and isn't worth it unless one BU is consistently driving peak. If one is — and we mean 30%+ of peak95 — then isolate that peak hour and surcharge it directly.