Per-Title Encoding Optimization: Content-Aware Bitrate Allocation for CDN Delivery

Last year I watched our CDN egress bill drop 31% in a single quarter without touching our cache hit ratio. The trick wasn't a new contract or a pricing tier — we'd finally ripped out our static bitrate ladder and replaced it with per-title encoding. For years we'd been shipping the same six renditions for everything from talking-head interviews to 60fps esports, and it was costing us both quality and cash. If you're still running a fixed ladder in 2026, you're almost certainly over-encoding simple content and under-serving complex scenes.

Why Fixed Bitrate Ladders Waste Bits and Hurt Quality

Most teams start with a familiar ladder: 1080p at 4.5 Mbps, 720p at 2.8 Mbps, 480p at 1.1 Mbps, and so on. It looks sensible on a slide, and it's easy to automate. The problem is that complexity isn't uniform. I've run encodes where a clean animation title hit VMAF 94 at just 1.8 Mbps for 1080p, while a grainy, high-motion soccer match needed 6.2 Mbps to crack VMAF 88 at the same resolution. With a fixed ladder you end up in a no-win situation — the cartoon wastes nearly 2.7 Mbps of unnecessary bits that your CDN has to store, replicate, and deliver, while the soccer feed looks blocky right when viewers care most.

We saw this play out painfully during a live tournament back in 2021. Our ops team had capped the top rendition at 5 Mbps to protect origin egress, assuming it was enough for 1080p. It wasn't. Fast pans turned into macroblocking mush and our QoE dashboards lit up with VMAF scores in the low 80s. Meanwhile, our children's catalog was quietly burning petabytes of delivery for bits nobody could perceive. That's the core trade-off per-title encoding solves: don't allocate bits by resolution, allocate them by what the content actually needs to reach a target quality. When we pair this with modern packaging like CMAF Packaging for CDN Delivery: Unified Streaming Format for HLS and DASH, we can serve those optimized renditions to both HLS and DASH clients without duplicating storage.

# Fixed ladder (what we retired) vs. what ffprobe tells you about complexity
# Quick complexity check with ffmpeg - not a full per-title pass, but useful for triage

ffmpeg -i input.mp4 -vf "select='eq(n\,0)',showinfo" -f null - 2>&1 | grep "mean.*PSNR"
ffprobe -v error -select_streams v:0 -show_entries frame=pict_type,best_effort_timestamp_time -of csv input.mp4 | head -n 20

# Extract spatial/temporal complexity hints for initial bucketing
ffmpeg -i input.mp4 -vf " SITI=print_format=json" -f null - 2>&1
# SI (Spatial Info) & TI (Temporal Info) via siti filter - low SI/TI = simple, high SI/TI = complex
# Use this to predict whether title needs 3 or 6 renditions before full convex hull search

How We Build a Content-Aware Pipeline Without Killing Turnaround

A proper per-title system isn't just "try a bunch of bitrates and pick the best." You're searching for the convex hull — the set of encodes that gives you the highest quality for the lowest bitrate across resolutions. Our current pipeline does it in two stages to keep costs sane. First, we run a fast pre-analysis pass: downscaled proxies, SI/TI scoring, and a shot detector. That tells us if a title is mostly static or if it's got frequent scene cuts that'll need separate optimization. I learned the hard way that you can't skip shot-based handling — treating a 90-minute film as one chunk averages out complexity and gives you a ladder that fits no single scene.

The second stage is the brute-force search, but bounded. For each shot or title segment, we encode at 5-7 CRF points per resolution (say, 360p through 1080p or 4K) and score with VMAF. We keep only the points that sit on the hull. In practice that means a simple interview might collapse to just three renditions, while a high-complexity action title keeps six. We target VMAF 93 for the top rung and space lower rungs by ~6-8 VMAF points, which keeps ABR switching perceptually smooth. The whole thing runs on spot instances and typically adds 35-45% to encode time versus a fixed ladder, but we get it back immediately in storage and delivery. If you're debating delivery protocols for those renditions, our breakdown in DASH vs HLS for CDN Streaming: Protocol Comparison and Selection Guide still holds — per-title works identically for both, but your segment duration and ABR logic will affect how often clients actually reach that top rung.

import json
import subprocess

# Simplified per-title hull search - production version runs parallelized
def encode_and_score(input_file, width, height, crf):
    output = f"/tmp/{width}x{height}_crf{crf}.mp4"
    # x264 example - x265/AV1 use similar CRF/CQ sweeps
    subprocess.run([
        "ffmpeg", "-y", "-i", input_file,
        "-vf", f"scale={width}:{height}",
        "-c:v", "libx264", "-crf", str(crf), "-preset", "medium",
        "-c:a", "aac", "-b:a", "128k", output
    ], capture_output=True)
    # Score with libvmaf
    result = subprocess.run([
        "ffmpeg", "-i", output, "-i", input_file,
        "-lavfi", "libvmaf=model=version=vmaf_v0.6.1:log_fmt=json:log_path=/tmp/vmaf.json",
        "-f", "null", "-"
    ], capture_output=True)
    with open("/tmp/vmaf.json") as f:
        vmaf = json.load(f)["pooled_metrics"]["vmaf"]["mean"]
    bitrate = get_bitrate(output) # helper that parses ffprobe bit_rate
    return {"resolution": f"{width}x{height}", "crf": crf, "vmaf": vmaf, "bitrate_kbps": bitrate}

# Build convex hull: keep only Pareto-optimal points (no other point has higher VMAF at lower bitrate)
def convex_hull(points):
    points = sorted(points, key=lambda x: x["bitrate_kbps"])
    hull = []
    max_vmaf = -1
    for p in points:
        if p["vmaf"] > max_vmaf:
            hull.append(p)
            max_vmaf = p["vmaf"]
    return hull

What Content-Aware Allocation Actually Saves on the CDN

Numbers matter more than theory here, so let's look at what we measured after moving ~12,000 assets from a fixed 6-rung ladder to per-title. We A/B'd the same catalog over 30 days, split by device type and region, and tracked origin storage, egress, and client QoE from our player analytics. The biggest surprise wasn't the bandwidth saving — it was how much rebuffering dropped on mid-tier connections. When your ladder matches content complexity, ABR algorithms don't get stuck hunting between a 1.1 Mbps rung that's too soft and a 2.8 Mbps rung that's just a bit too heavy for a 3G link in India. Our per-title ladders filled that gap with intermediate points we would've never included manually.

There's a catch you should plan for: per-title increases manifest complexity. Instead of one HLS variant playlist template, you're now generating manifests dynamically, and your origin needs to handle a variable number of renditions per asset. That changes cache key cardinality and can fragment your edge cache if you aren't careful with surrogate keys. We also had to update our Low-Latency HLS Configuration for CDN: Sub-3-Second Live Streaming setup for live per-title — for VOD it's straightforward, for live you need segment-aware analysis or you'll add too much latency. As the HTTP Working Group Specifications (https://httpwg.org/specs/) remind us, caching semantics still rely on correct Cache-Control and CDN-Cache-Control headers when rendition counts vary, and guidance from web.dev Articles (https://web.dev/articles/) on responsive media helped us tune client-side switching thresholds to match the new ladders.

Metric (30-Day Avg, VOD Catalog) Fixed Ladder (6 Rungs) Per-Title Optimized (3-6 Rungs) Delta
Avg. Delivered Bitrate (p75, All Viewers) 3.82 Mbps 2.61 Mbps -31.7% egress
Top Rung VMAF (Complex Titles, 1080p) 84.2 92.6 +8.4 points
Origin Storage per Hour of Source 6.80 GB 4.15 GB -39.0% storage
Rebuffering Ratio (<3 Mbps connections) 1.82% 0.94% -48.4% rebuffering
Avg. Renditions Stored per Title 6.0 4.1 -31.7% variants

Putting Per-Title Into Production Without Blowing Up Your Transcode Farm

You don't need Netflix-level infrastructure to ship per-title. We've been running it with a pretty lean setup: a probe stage that runs ahead of the full transcode, plus a convex hull builder that picks the ladder. The trick is not to brute-force 10 encodes per title. Run a fast first-pass at 8x speed using a downscaled proxy (say, 720p at CRF 28) to estimate complexity — spatial energy, temporal energy, and a quick VMAF probe. That gives you a complexity score in under 60 seconds for a 45-minute episode. From there, you predict where the RD curves will bend instead of encoding every rung blindly.

In our current VOD pipeline, that probe feeds directly into the packager. If the content is low complexity — talking heads, slides, slow drama — we collapse the ladder. We went from a fixed 6-rung ladder (400 kbps to 5800 kbps) to a dynamic 4-5 rung ladder where the top rung might only be 3500 kbps at VMAF 93. For high-complexity content like sports or confetti-heavy concerts, we do the opposite and stretch the top to 7500 kbps and add an intermediate rung at 5200 kbps to avoid that ugly quality cliff. This integrates cleanly with a modern CDN Live Streaming Architecture: Ingest, Transcode, Package, and Deliver at Scale — your ingest and transcode stay the same, you’re just making the ABR ladder an output variable instead of a static config. Turnaround stays under 1.4x real-time on a single c7i.4xlarge per title.

On the CDN side, the win is immediate but you’ve got to handle manifest variance. Don't hardcode your HLS/DASH manifests. Generate them from the convex hull JSON so the player only sees rungs that actually exist for that title. We cache those manifests at the origin with a surrogate key per title and purge on re-encode. That keeps edge hit ratios above 96% even with thousands of unique ladders. Here's the stripped-down logic we use to build the ladder after the probe:

# per_title_builder.py - convex hull pruning with VMAF target
import json

def build_ladder(complexity_score, vmaf_targets=[88, 92, 94]):
    # complexity_score: 0-100 from probe (SI/TI + fast VMAF)
    base_rungs = [
        {"h": 360, "br": 400}, {"h": 480, "br": 800},
        {"h": 720, "br": 1500}, {"h": 1080, "br": 3000},
        {"h": 1080, "br": 4500}, {"h": 1440, "br": 6000},
    ]
    # scale bitrates by complexity, then prune by VMAF delta
    scaled = []
    factor = 0.65 if complexity_score < 30 else 1.35 if complexity_score > 75 else 1.0
    
    for rung in base_rungs:
        adj_br = int(rung["br"] * factor)
        # simulate VMAF; in prod, this is actual encode + libvmaf
        est_vmaf = estimate_vmaf(rung["h"], adj_br, complexity_score)
        scaled.append({**rung, "br": adj_br, "vmaf": est_vmaf})

    # keep only rungs with >4 VMAF delta (convex hull)
    hull = [scaled[0]]
    for r in scaled[1:]:
        if r["vmaf"] - hull[-1]["vmaf"] >= 4.0 and r["vmaf"] <= 95:
            hull.append(r)
    
    # ensure top rung hits quality target
    if hull[-1]["vmaf"] < 92:
        hull[-1]["br"] = int(hull[-1]["br"] * 1.25)
    return hull

Advanced Patterns, Edge Cases, and When Per-Title Breaks

Per-title isn't magic — it'll bite you on a few content types if you're not careful. Film grain is the classic killer. Your probe will read it as high complexity and push bitrates 40% higher, but viewers often can't see the difference past VMAF 90 because the grain itself masks artifacts. We've learned to add a grain detection flag (using variance of luma at 1080p) and cap the top rung, or better, run a light denoise + synthetic grain workflow. Same story with anime and screen-share content: super sharp edges but low motion. A standard SI/TI score overestimates what you need. For those, we pin the ladder to 3 rungs and let CRF do the work — you'll save 55-60% versus a fixed ladder without a single complaint.

For live, pure per-title is too slow, so we shift to per-shot or per-segment. Instead of analyzing the whole title, analyze the last 4-6 GOPs (about 8-12 seconds) and adjust the next segment's ladder. It’s not as efficient as VOD — you’ll save maybe 12-18% instead of 30% — but it keeps you within your 3-5 second glass-to-glass budget. This pairs well with chunked CMAF; if you're exploring that path, check our notes on Low-Latency DASH with CMAF Chunks: Achieving Near-Real-Time CDN Delivery. And if you're doing time-shifted live with per-segment ladders, your DVR window gets tricky — you need consistent initialization segments. Our fix is detailed in CDN DVR Functionality at the Edge: Time-Shifted Viewing for Live Streams, where we freeze the ladder for the DVR window and only optimize the live edge.

Troubleshooting-wise, watch your ABR logic after you ship. Most players assume evenly spaced rungs. When you give them a ladder like 480p @ 600 kbps, 720p @ 1100 kbps, 1080p @ 3100 kbps, some throughput-based ABR algorithms will oscillate. We had to retune our player to use buffer-based decisions and add a 15% hysteresis on up-switches. Also, monitor VMAF drift. We log actual VMAF vs. predicted VMAF per encode — if drift exceeds 3 points for more than 5% of titles, your probe model is stale. Retrain it monthly with your latest content mix. Don't just look at averages; look at the P95 encode — that's where your support tickets come from.

Frequently Asked Questions

How much extra compute does per-title actually cost?

Less than you'd think if you do it right. A naive approach that encodes 8-10 full rungs and measures VMAF will cost you 3-4x your current transcode bill. With a fast proxy probe and convex hull pruning, we’re at 1.2-1.4x. The probe itself runs at 120-180 fps on a CPU instance and costs pennies. You recoup that in the first week of CDN egress savings — we’ve seen a 28% reduction in origin-to-edge bytes for a catalog of 12k titles, which dwarfs the extra 20 cents per hour of transcode.

Can I use per-title for live or is it VOD only?

VOD is the easy win, but you can do per-shot for live. You won’t build a perfect convex hull mid-stream, so you run a sliding window analysis on 2-4 second segments and adjust bitrate, resolution, or CRF on the fly. Expect 15-20% savings versus a fixed ladder, not 35%. For sub-4-second latency workflows, keep a fallback fixed ladder ready — if your analyzer lags, fall back to the safe ladder rather than dropping frames.

Does per-title work the same for AV1 / HEVC as it does for H.264?

The principle is identical, but the savings are bigger with modern codecs. With H.264, we typically save 25-35% at the same VMAF. With HEVC, that jumps to 35-45%, and with AV1 it’s 40-50% because the codec’s tools handle low-complexity content even more efficiently. The catch is encode time — AV1 per-title without a proxy probe can be brutally slow. Use SVT-AV1 preset 6 or 8 for the probe and only do a full encode for the final hull rungs.

Will fragmented ladders break my players or analytics?

They can if you’re not prepared. Older Smart TV players that expect exactly 6 HLS variants will sometimes misbehave. Test against your top 20 devices — we found 2018-era Samsung Tizen needed a manifest shim. For analytics, stop grouping by “1080p bitrate” and group by “top rung VMAF” instead. Otherwise your QoE dashboards will show wild bitrate variance that’s actually intentional quality optimization. Tag each session with ladder_id so you can A/B per-title vs. fixed for the same content.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker