5G NR — end-to-end packet journey & loss anatomy
A single-file in-browser visualizer of how an IP packet actually gets from a 5G base station to a phone — and the six different places it can disappear on the way. n78 SA cell (3.5 GHz / 100 MHz / 30 kHz SCS / 273 PRBs), one attached UE, full DL data path through PDCP → RLC AM → MAC → PHY, HARQ + RLC ARQ + PDCP discard timer doing their jobs, an inter-gNB handover with Xn-U PDU forwarding and PDCP SN-based recovery, an AMC loop driving the MCS from CQI reports, and a tiny TCP model (Reno + BBR) on top so you can see how the transport reacts to each kind of loss.
The default script walks the canonical edge cases:
boot → UE attaches, MCS 13 (64-QAM CR 466/1024)
clean link → one packet through the whole stack, ACKed
inject 5% PHY BLER → HARQ kicks in, RV 0→2→3→1, Chase combine
AMC drops MCS 13 → 9 → BLER returns to 10% target
HARQ exhaustion → 4 attempts fail; RLC ARQ retransmits
RLC maxRetxThreshold (8) → RLC tears down; PDCP discard timer is the last line
PDCP discard fires → packet genuinely lost — application sees a gap
TCP reacts → Reno halves cwnd; BBR barely flinches
stale CQI report → AMC over-shoots to MCS 22; BLER spikes ~40%
intra-gNB beam switch → no PDCP gap (same SDU map)
inter-gNB handover (Xn) → source gNB tunnels in-flight PDUs to target
RACH gap at target gNB → ~25 ms with no DL throughput
PDCP SN-based recovery → STATUS REPORT heals the gap, no upper-layer loss
cell congestion → 2nd UE; URLLC preempts eMBB allocations
UPF / N3 backhaul ECN → AQM marks CE on IP; Reno halves, BBR cuts pacing
idle → free play
What this actually is (for someone who’s never built radio)
You probably think of “the radio dropped my packet” as one thing. It is not. Inside a 5G network, an IP packet that the application sees as lost has fallen through somewhere between two and six layers of protection. Each of those layers exists because the layer above it isn’t tolerant of what the layer below it does — and the cost of each layer is more delay, more state, more battery on the phone.
The protection stack going down is roughly:
- PHY (layer 1). LDPC parity + modulation puts your bits on subcarriers and transmits them over a noisy air interface. The receiver runs a CRC; if it fails, this transmission is officially gone. PHY’s job ends here — it doesn’t retransmit.
- HARQ (in MAC + PHY). A second CRC pass plus a redundancy version protocol. On a NACK, the transmitter sends different parity bits (RV 0 → 2 → 3 → 1) instead of the same data again. The receiver combines all attempts (“Chase combining” or “incremental redundancy”) — so even if no single attempt would have decoded, the combined energy might. Round-trip ≈ 8 ms. Up to 4 attempts. If all 4 fail, HARQ exhausts.
- RLC ARQ (layer 2). A slower retransmit loop, in software. The
transmitter buffers PDUs; the receiver sends
STATUS PDUslisting missing sequence numbers. RLC retransmits. There’s amaxRetxThreshold(default 8); if you hit it, RLC gives up. - PDCP discard timer (layer 2 above RLC). A wall-clock deadline. If a PDCP SDU hasn’t been acknowledged by then, throw it away. For most bearers that’s 100 ms. The deadline exists so very late deliveries don’t damage real-time application semantics (a 300 ms old voice packet is worse than no voice packet).
- TCP / QUIC retransmit (in the host). End-to-end. If the IP packet doesn’t make it through anything below, the transport eventually notices and retransmits. The visible side-effect is throughput collapse, not application data loss.
The simulator surfaces all five of those layers. The Stack view shows
one packet at a time walking down the gNB stack and up the UE stack with
headers added at every layer; the headlines are which layer it currently
sits in and which layer killed it if it doesn’t make it. The HARQ view
shows 16 parallel processes — each one a slot-by-slot timeline of
TX → ACK/NACK → retransmit, colour-coded by redundancy version. The
Scheduler view is a frequency × time grid (PRB × slot) showing which UE
got which PRBs in each slot, and how URLLC strict-priority preemption looks
on screen when the cell is congested. The Loss view is the cause
attribution: of every packet you’ve lost, where did it die — a pie chart
plus a running event list. The TCP view is two cwnd curves on the same
simulated radio: Reno (treats every loss as congestion) and BBR (treats loss
as noise; uses bandwidth × min-RTT). Same physics; very different reactions.
Two specific things you can only see by watching them happen:
- What an inter-gNB handover actually loses. Toggle the
Handoverknob. The source gNB stops scheduling the UE, tunnels any unacknowledged PDCP PDUs to the target gNB over Xn-U, and the UE has to perform RACH on the target. RACH + scheduling-request + buffer-status-report takes about 25 ms. During those 25 ms, no DL throughput. PDCP PDUs queue up in the Xn-U buffer; the ones whose discard timer expires first are genuinely lost. The PDCP STATUS REPORT once RACH finishes recovers the rest. - Why TCP cares about which layer lost the packet. Toggle
HARQ killfor 80 ms and watch what happens. Many packets get NACKed → retransmitted → eventually delivered with extra latency (no loss). One or two trip thepdcp_discardcounter. The TCP view shows Reno’s cwnd halve on each genuine loss; BBR’s cwnd barely moves. Same radio; very different throughputs.
Views
| View | What it shows |
|---|---|
| Stack | Vertical TX column (gNB) on the left, RX column (UE) on the right, the air interface band across the middle, an event-trail panel in the centre, and a loss-cause running tally below. Hover any layer for its role + relevant 3GPP spec ref. Click the canvas to inject a single test packet. |
| HARQ | 16 HARQ processes as 16 timeline rows of TX (RV0) → ACK/NACK → retx (RV2/3/1) → …, plus a UE state panel with CQI, MCS, code rate, buffer queue depth, in-flight processes, last delivery RTT, and a rolling 200-TB BLER histogram. |
| Scheduler | PRB × slot allocation grid: 273 PRBs of frequency × the last 80 slots of time, cells coloured per slice (URLLC violet, eMBB blue, mMTC pink). URLLC strict-priority preemption is visible whenever you flip Congest. Side panel lists scheduler inputs (BSR, SR, CSI, QoS-flow priority). |
| Loss | Pie chart over the six loss causes (PHY BLER · HARQ exhaustion · RLC maxRetx · PDCP discard · Handover gap · Buffer drop) + a counts table + a timeline of recent lost events + a “specs cited” panel with the exact 3GPP refs. |
| TCP | cwnd over time and a throughput proxy (cwnd ÷ RTT) for Reno and BBR side by side on the same simulated radio. Side panel explains the models. |
What’s “precise” about it
- Real HARQ semantics. 16 processes per UE per HARQ entity (TS 38.214
§5.1.7). Four attempts with the standard RV cycle
[0, 2, 3, 1]. Chase combining modelled as a multiplicative BLER reduction per attempt. Feedback RTT ≈ 8 slots ≈ 4 ms at 30 kHz SCS. - Real RLC AM ARQ.
maxRetxThreshold = 8(one of the{1, 2, 3, 4, 6, 8, 16, 32}values in TS 38.322 §5.3). RLC entity declares failure to PDCP on threshold. - Real PDCP discard timer.
pdcp-DiscardTimer = 100 ms. Counts down from PDCP SDU entry; if not ACKed at zero, the SDU is dropped and counted as PDCP-discarded loss. One of{10, 20, 30, 50, 80, 100, 150, 300, 500, 750, 1500 ms, infinity}in TS 38.323 §7.3. - Real PRB grid. 273 PRBs is exactly 100 MHz at 30 kHz SCS (TS 38.101-1).
TB size approximated as
PRBs · 12 subcarriers · 14 OFDM symbols · bits/sym · code rate. - Real MCS values. Subset of TS 38.214 Table 5.1.3.1-1 (64-QAM table). MCS index 13 = 64-QAM CR 466/1024; AMC drops to MCS 9 = 16-QAM CR 378/1024 under BLER pressure.
- Real handover sequence. Source-side: forward in-flight PDCP PDUs to target via Xn-U. Target-side: RACH delay ≈ 25 ms before scheduling resumes. PDCP STATUS REPORT (TS 38.323 §5.1.2) heals the surviving SDUs.
- Real BLER target. AMC aims for ~10% first-transmission BLER per TS 38.214 §5.1.4 — high enough to keep MCS aggressive, low enough that HARQ catches most failures.
Controls
| Key / mouse | Action |
|---|---|
space | Run / pause the script |
s | Step one engine tick (0.5 ms simulated — one NR slot) |
shift+s | Step ×10 |
r | Reset (clear HARQ, RLC, PDCP, scheduler grid) |
v | Cycle Stack → HARQ → Scheduler → Loss → TCP |
| click on the Stack view | Inject a fresh test packet |
| hover anything | Context tooltip (HTML on toolbar, canvas #insp on every region) |
| toolbar knobs | BLER 5% · HARQ kill · Handover · Congest · UPF ECN · CQI lie |
References
The 3GPP specs the sim quotes:
- TS 38.211 — NR Physical channels and modulation (RV table, PT-RS).
- TS 38.212 — NR Multiplexing & channel coding (LDPC).
- TS 38.213 — NR Physical-layer procedures for control (HARQ-ACK timing).
- TS 38.214 — NR Physical-layer procedures for data (MCS table, HARQ ≤16 processes).
- TS 38.321 — NR MAC protocol (BSR/SR, HARQ entity).
- TS 38.322 — NR RLC protocol (AM ARQ,
maxRetxThreshold). - TS 38.323 — NR PDCP protocol (
pdcp-DiscardTimer, STATUS REPORT). - TS 38.331 — NR RRC (beam-failure recovery, mobility).
- TS 38.413 — NG-AP / Xn-AP (handover signalling).
- TS 23.501 — System architecture (QoS flows, DRBs, slicing).
- RFC 5681 / 6582 — TCP Reno / NewReno.
- draft-cardwell-iccrg-bbr — TCP BBR.
How it works under the hood
Single HTML file, no build, no server. The engine state (UEs, HARQ
processes, RLC buffers, PDCP windows, PRB allocation history, TCP cwnd
samples, loss counters, scenario timers) lives inside a small IIFE. The
visualizer is a requestAnimationFrame loop wrapped in try/catch so a
single regression can’t lock up the UI: every frame it reads the engine
state and routes to one of five drawX() functions over a 1180 × 700
logical canvas.
One simulated tick = one NR slot at 30 kHz SCS = 0.5 ms. At the default speed slider position the engine runs ~10 simulated slots per wall-clock second.