CDN Performance on Mobile Networks: 4G, 5G, and the Challenges of Wireless Delivery

I was on the BART last year watching our real-user metrics spike while a major live stream stuttered for users on the move. My laptop was tethered to my phone on a solid 5G signal, yet the video kept rebuffering every 20 seconds. Back at the office, the wired tests looked perfect. That disconnect is exactly why mobile CDN delivery is its own discipline — you can't treat a radio link like a shaky Ethernet cable and expect the same tuning to work.

Why a Radio Link Isn't Just a Slow Wire

For years I tuned CDN edges assuming the last mile was fiber or DOCSIS: stable RTT, predictable loss under 0.1%, and congestion that mostly lived in the middle mile. Wireless throws all of that out. The scheduler in an eNodeB or gNodeB decides who gets to transmit every millisecond, signal strength swings as you move a few feet, and the device itself flips between radio states to save battery. I've seen a single user session swing from 35ms to 290ms RTT in under ten seconds without losing a bar of signal, just because the cell got crowded.

The worst offender is the bufferbloat hiding inside the carrier network. Base stations maintain deep per-device queues to keep the air interface busy, which means TCP thinks the path can handle more than it really can until latency explodes. Your standard Cubic or BBR congestion controller fills that buffer, you get a latency spike, then packet loss, then a stall. It's the opposite of the wired world where loss is a clear signal. On mobile, delay is often the first sign you're in trouble. That's why understanding the Bandwidth-Delay Product and CDN Tuning: Filling the Pipe Without Overwhelming It matters so much here — if your edge sends based on wired BDP assumptions, you'll routinely overwhelm the radio queue before the client ever acks.

Then there's the RRC state machine. When a phone hasn't sent data for a few seconds, it drops from RRC_CONNECTED to RRC_IDLE to save power. The next request pays a 80-150ms promotion delay just to get back to active, which murders your TTFB. We caught this for years on API calls for an e-commerce client — their "fast" edge was blamed, but the trace showed the delay was all RRC promotion. Once we understood that, we stopped chasing edge latency and started looking at TCP Window Scaling and Its Impact on CDN Throughput: Why Defaults Fall Short, because a small initial window plus a promotion penalty means you can't get enough bytes in flight to use the 4G/5G link you just paid to wake up.

# /etc/sysctl.d/60-mobile-edge.conf - tuned for high-RTT variance, not wired LAN
# I've used this baseline on PoPs serving >60% mobile traffic
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_adv_win_scale = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_no_metrics_save = 1

# BBRv2 handles bufferbloat better than Cubic on variable radio links
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq

# Increase initial window - helps overcome RRC promotion + small IW
# Check HTTP Working Group Specifications (https://httpwg.org/specs/) for IW guidance
net.ipv4.tcp_initial_cwnd = 32
net.ipv4.tcp_notsent_lowat = 16384

How 4G, 5G NSA, and 5G SA Behave Differently Under CDN Load

4G LTE is still the workhorse, and honestly, it's more predictable than 5G for CDN planning. In our US and EU measurements, typical 4G RTT sits around 45-75ms to the nearest edge, with goodput of 15-40 Mbps down on a non-congested cell. Uplink is often the bottleneck at 5-15 Mbps, which hurts QUIC handshakes and upload-heavy beacons. Packet loss is usually 0.5-1.5% under mobility, and jitter is moderate. The thing that surprised me early on was how much carrier aggregation helps — a 3x CA phone can triple throughput without changing latency, so your throughput-based ABR logic gets fooled into thinking the link is stable when it's actually just aggregated and still jittery.

5G is where it gets messy because "5G" isn't one thing. Non-Standalone (NSA) still anchors on a 4G core, so you get 5G NR speed for data plane but 4G-like control plane latency. I ran drive tests in Chicago last fall with an NSA device showing 180 Mbps down but 38-55ms RTT — faster, but not lower latency. Standalone (SA) with a 5G core is different; we saw 18-30ms RTTs to the same edge PoP, but coverage is patchy. And mmWave, when you actually find it, is wild: 600+ Mbps line-of-sight, then it drops to near zero behind a hand or a window. Your CDN's throughput estimates can swing 10x in a block. You can't set a single bitrate ladder for "5G users" and walk away.

This variability is why we moved most mobile traffic to HTTP/3 and QUIC where we could. QUIC's migration and better loss recovery helps a lot when the link hiccups or the user switches cells, and 0-RTT resumption shaves off that RRC promotion cost on repeat visits. The HTTP Working Group Specifications for QUIC and HTTP/3 spell out how the transport handles this, but in practice, you still need to monitor connection migration failures — we've seen middleboxes on older 4G carriers break UDP. For progressive enhancement, I still keep solid HTTP/2 fallback and use client hints to serve lighter variants, following guidance from web.dev Articles on adaptive serving. One practical win: coalescing small assets and enabling Early Hints cut our 4G-induced render blocking significantly.

// RUM snippet I've used to correlate radio type with CDN performance
// Uses Network Information API + Navigation Timing + Resource Timing
function captureMobileCDNMetrics() {
  const conn = navigator.connection || {};
  const timing = performance.getEntriesByType('navigation')[0];
  
  const metrics = {
    effectiveType: conn.effectiveType, // '4g', '3g' etc. - not real 5G, but useful
    rtt: conn.rtt, // ms estimate from the browser
    downlink: conn.downlink, // Mbps estimate
    saveData: conn.saveData,
    // Real RTT measured at app layer - more reliable than conn.rtt
    ttfb: timing ? timing.responseStart - timing.requestStart : null,
    tcpHandshake: timing ? timing.connectEnd - timing.connectStart : null,
    // Track retransmits/stalls via Resource Timing if available
    transferSize: timing ? timing.transferSize : null
  };

  // Only beacon if we have a meaningful sample - avoids RRC_IDLE noise
  if (metrics.ttfb !== null && metrics.ttfb < 5000) {
    navigator.sendBeacon('/cdn-rum', JSON.stringify(metrics));
  }
}
captureMobileCDNMetrics();

What to Measure When Averages Lie on Wireless

If you take one thing from a decade of chasing mobile ghosts, it's this: averages are useless. I don't care that your average 4G TTFB is 85ms — your p95 is probably 450ms and that's where users churn. Wireless performance is a long-tail distribution, and CDN dashboards that only show means will hide the RRC tails, the bufferbloat spikes, and the cell-edge users entirely. We switched our SLOs to p75/p90/p95 for TTFB and rebuffer ratio segmented by radio type, and suddenly the "noisy" alerts became actionable. You also need to separate idle vs. active radio samples, or you'll blame your edge for a promotion delay it didn't cause.

Beyond standard RTT and throughput, I watch three mobile-specific signals. First, stall rate and duration from the player — not just rebuffer count — correlated with RTT Measurement Methodology for CDN: Getting Accurate Round-Trip Times at the kernel level versus the app level, since carrier buffers make those diverge. Second, retransmit rate and bytes discarded at the edge, which tells you if your congestion window is too aggressive for the link. Third, RRC tail energy: the extra seconds the radio stays active after a transfer, which we infer from inter-request timing. That tail kills battery and makes users on data saver bounce. It changed how we batch beacons and prefetch.

When we benchmark a new PoP or a new congestion control, we baseline against real-world wireless profiles, not iperf on a clean lab line. The numbers below are rounded medians from our RUM dataset last quarter (US mobile, 15M samples, edge within 200km). They're not lab ideals — they include handovers, congestion, and that guy streaming in an elevator. Use them to set realistic timeouts, initial windows, and ABR thresholds rather than copying wired defaults.

Network Profile Median RTT to Edge Median Downlink Goodput Median Packet Loss Edge TTFB p95 (1KB object)
4G LTE (good signal, 2x CA) 52 ms 28 Mbps 0.8% 380 ms
4G LTE (cell edge / congested) 98 ms 6 Mbps 2.4% 890 ms
5G NSA sub-6 GHz 42 ms 95 Mbps 0.6% 290 ms
5G SA sub-6 GHz 24 ms 140 Mbps 0.4% 165 ms
5G mmWave (line-of-sight) 18 ms 420 Mbps 1.1%* 210 ms*

Tuning the Edge Stack for a Link That Won't Sit Still

You can't fix radio variability from the origin. You fix it at the edge, and you fix it by assuming the client is hostile, mobile, and lying about its bandwidth. For years we tuned CDN stacks for stable, wired last-miles where Cubic and an initcwnd of 10 was good enough. On 4G and especially 5G SA, that config leaves a ton of performance on the floor. I've seen p95 TTFB drop from 180ms to 65ms on a live 5G SA segment just by switching the congestion controller and enabling proper pacing — no PoP move at all.

The first lever is transport. If you're still serving mobile traffic over TCP Cubic without pacing, you're asking for bufferbloat at the eNodeB/gNodeB. BBRv2 or BBRv3 handles wireless bottleneck estimation far better because it models the delivery rate instead of just reacting to loss. Pair it with pacing, ECN, and a bumped initcwnd. On lossy 4G links with 30-50ms jitter, Cubic will halve its window on a single radio retransmission that wasn't even congestion. BBR won't. And for 5G SA, you want QUIC wherever you can get it — 0-RTT resumption and connection migration are not just nice-to-haves when a user hands over between cells every 8 to 12 seconds on a train. In our tests, QUIC cut connection re-establishment from 90-120ms to under 15ms during handovers.

The second lever is what you send and when. Don't ship a 2.5MB hero image at once and hope TCP figures it out. Use HTTP/3 prioritization properly, Early Hints (103), and adaptive chunking. We chunk video segments to 1s instead of 4s for mobile, and cap initial image payloads to ~150KB before lazy-loading the rest. It keeps the congestion window from collapsing and gives the radio scheduler smaller, more schedulable bursts. Combine that with Brotli-11 at the edge for text assets — you'll save that extra round trip that matters so much when your CDN Percentile Latency Analysis: What p50, p95, and p99 Actually Tell You shows a p50 of 45ms but a p99 of 980ms.

# /etc/sysctl.d/99-mobile-edge.conf - Tested on Ubuntu 22.04 / Edge PoPs
# Use BBRv3 + FQ pacing for wireless last-mile

net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
net.ipv4.tcp_ecn = 1
net.ipv4.tcp_fastopen = 3

# Bump initcwnd from 10 to 32 for 5G SA/NR - huge win for small objects
# Applied per-route, not global:
# ip route change 0.0.0.0/0 via 10.0.0.1 initcwnd 32 initrwnd 32

# Nginx / QUIC tuning for handover-heavy traffic
# quic_retry on;
# quic_gso on;
# http3_max_concurrent_streams 100;

# Enable pacing at 1.2x BBR rate via tc-fq (check with ss -tie)
# sysctl -w net.ipv4.tcp_pacing_ss_ratio=120
# sysctl -w net.ipv4.tcp_pacing_ca_ratio=120

When Wireless Delivery Falls Apart: Jitter, Reordering, and Handover Storms

Once you've got the basics tuned, the failures you see won't be average throughput — they'll be tail events that your dashboards average away. The classic is jitter-induced rebuffering. Averages lie hard on wireless, and CDN Jitter Measurement and Its Impact on User Experience matters more than mean latency here. We've watched a 5G NSA cell deliver 400 Mbps median with 12ms jitter, then spike to 80-110ms inter-arrival jitter for 3 seconds when the user moved behind a building. The player had enough bandwidth, but the jitter emptied the 2-second ABR buffer and caused a stall. Fix: don't just monitor throughput. Track packet inter-arrival jitter at the edge (via qlog or tcp_info), and feed that into your ABR logic. If jitter p95 exceeds 35ms, force a downshift one rendition early — it's better than a stall.

Then there's packet reordering, the silent throughput killer everyone blames on loss. On 4G with carrier aggregation and on 5G NSA with dual connectivity (LTE + NR legs), packets routinely arrive out of order by 3-10 positions. Cubic and even BBRv1 interpret that as loss, trigger fast retransmits, and collapse. If you're seeing 2-5% retransmits but <0.2% actual loss on the radio trace, you're looking at reordering. I recently debugged a PoP where throughput for mid-size 500KB objects was 40% lower on NSA than SA — same signal strength. The culprit was an overly aggressive reordering window. We increased `tcp_reordering` to 20 and enabled RACK (`tcp_recovery=1`), and throughput recovered. For a deeper dive into why this happens, see Packet Reordering Effects on CDN Performance: The Hidden Throughput Killer.

Finally, troubleshoot handovers, not just PoPs. Your AnyCast may be perfect, but if the UE hops from gNB-A to gNB-B, its source IP doesn't change — your TCP session does not migrate. The old path's packets are now dead air for 40-80ms while the RAN forwards. With TCP, that's a timeout. With QUIC connection migration, it's survivable, but only if your edge supports it and your LB doesn't pin on 5-tuple. Checklist when p99 spikes correlate with mobility: 1) Verify QUIC migration is enabled end-to-end (client, LB, origin). 2) Check that your edge doesn't drop UDP on handover due to firewall flow timeout (we set ours to 60s+). 3) Look for TLS session resumption misses — if 0-RTT fails, you're paying a full 1-RTT handshake right when the radio is weakest. Fix those, and that ugly 800ms to 1200ms tail starts to look like a normal 90ms line.

Frequently Asked Questions

Should I use different CDN settings for 4G vs. 5G SA users?

Yes, if you can detect it. You don't get an explicit "this is 5G SA" header, but you can infer it from Client Hints (ECT, RTT, downlink) and TCP info. We serve QUIC + BBR with initcwnd 32 to users with RTT <20ms and downlink >15 Mbps, and fall back to BBR with initcwnd 20 and more conservative pacing for typical 35-55ms 4G profiles. Don't over-segment though — two well-tuned profiles beat five brittle ones.

Why does my 5G speed test show 800 Mbps but my CDN video still stalls?

Speed tests measure peak burst throughput over 5-10 seconds on a warmed-up connection. Video and web need consistent low jitter and low tail latency for small bursts. We've seen 5G NSA push 600 Mbps on speedtest.net while delivering 500KB objects at only 8 Mbps effective because of reordering and jitter. Stalls are almost never about average bandwidth — they're about packet inter-arrival variance and handover gaps emptying your playback buffer.

Is QUIC really worth it for mobile CDN delivery yet?

For mobile, absolutely. The win isn't just 5-10% faster handshakes. It's connection migration during cell handovers, better loss recovery than TCP, and no head-of-line blocking. In our A/B tests on commuter rail routes, QUIC reduced rebuffer rate by 22-28% and cut p95 page load by ~110ms compared to HTTP/2 over TCP. The caveat: you need to tune UDP — increase buffers, enable GSO/GRO, and make sure your DDoS mitigation doesn't rate-limit QUIC.

How do I tell if bufferbloat or reordering is hurting my mobile performance?

Check two signals at the edge. For bufferbloat: high tcp_info RTT variance (rtt_var >25ms) with growing delivery rates and no loss. For reordering: high retransmits with DSACKs indicating spurious retransmits, and qlog showing packets received out of order but no real loss on retransmission trace. If retransmits drop when you enable RACK and increase tcp_reordering, it was reordering all along.

Related Articles

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