ROHC Header Compression in LTE 4G
Robust Header Compression profiles and why they matter for VoLTE and small packets.
A VoLTE voice packet carries roughly 32 bytes of actual speech — then bolts on a 40-byte IPv4/UDP/RTP header (60 bytes for IPv6) to carry it across the network. That is more overhead than payload, repeated every 20 ms. ROHC (RObust Header Compression) lives inside PDCP and squeezes those 40–60 header bytes down to just one to three, by noticing that almost every field either never changes or changes in a perfectly predictable pattern. This page is grounded in TS 36.323 (LTE PDCP), which imports the IETF ROHC framework of RFC 5795 (the framework) and RFC 3095 (the original profiles).
Introduction
ROHC is a compression function that sits inside the PDCP sublayer of the LTE user plane. It runs on the transmitting side (the compressor) and the receiving side (the decompressor) of a Data Radio Bearer, and its only job is to shrink the IP/UDP/RTP (or IP/UDP, IP, TCP) header that wraps every user-plane packet. The payload is untouched; only the header is compressed.
It matters most in the moments where the air interface is busiest and the headers are largest relative to the payload — above all VoLTE, where a tiny, constant-rate codec frame is sent 50 times a second. In that traffic pattern the header is as big as the voice it carries, so compressing it is not a refinement but the difference between packet voice being economical or wasteful. ROHC is configured by RRC per DRB at bearer setup, activates as soon as both ends agree, and quietly rebuilds itself whenever the radio link disturbs it.
Because it is a conceptual, structural function rather than a step-by-step call flow, the way to understand ROHC is to follow its state: how a compressor and decompressor build a shared context, ride on it to send almost nothing, and recover that context when the radio drops packets underneath them.
On this page
Why ROHC is needed
In plain words: imagine narrating a train journey where you begin every single sentence with your full name, home address, and destination before adding the one new fact — "we just passed a bridge." ROHC says the long preamble once, both ends memorise it, and after that every packet is just "…passed a bridge, …a field, …a station" — the plot only, never the preamble again.
Radio bytes are the most expensive bytes in the system. On a VoLTE flow the media payload is tiny and roughly constant — an AMR codec frame is on the order of 20–32 bytes every 20 ms — but every one of those packets still drags a full protocol-stack header behind it. Sending 40 bytes of header to deliver 32 bytes of voice wastes air-interface capacity, drains UE battery, and scales badly across the thousands of simultaneous voice users a cell is expected to hold.
A PDCP function, applied to user-plane DRBs only, that replaces the large IP/UDP/RTP (or IP/UDP, IP, or IP/TCP) header with a small compressed header plus a per-flow context identifier (CID).
Most header fields are either constant for the life of a flow (source/destination IP addresses, ports, RTP SSRC) or change by a predictable increment (RTP sequence number, timestamp, IP-ID). Re-sending them in full on every packet is pure redundancy.
The compressor and decompressor each build an identical context from the first few packets. After that the compressor sends only the fields that changed unpredictably — often the header collapses to a single byte.
The waste is easiest to see laid out byte by byte. A bare IPv4 header is 20 bytes, UDP adds 8, and RTP adds 12, for 40 bytes of header wrapped around a ~32-byte voice sample. On IPv6 the base header alone is 40 bytes, pushing the total to 60. In both cases the header is as large as or larger than the thing it is carrying, and it is sent unchanged 50 times a second.
The intuition: tell the other end the whole story once, then send only the plot twists. ROHC transmits the full header up front to establish shared state, then rides on that state to send tiny deltas — usually just a compressed sequence number.
Where ROHC Sits in PDCP
ROHC is one of the PDCP sublayer's user-plane functions. On transmit, PDCP receives an IP packet as a PDCP SDU, runs the ROHC compressor to produce a compressed header, then applies ciphering and prepends the PDCP header. On receive the order reverses: PDCP removes its header, deciphers, then runs the ROHC decompressor to reconstruct the original full header before delivering the packet to the upper layers. Header compression therefore happens on the plaintext, before ciphering — the compressed header is what actually gets encrypted and sent over the air.
ROHC applies to Data Radio Bearers (DRBs) mapped to RLC AM or UM — never to Signalling Radio Bearers. SRBs carry RRC and NAS signalling, which has no redundant IP/UDP/RTP stack to exploit and must stay maximally reliable, so header compression would buy nothing and risk much. Whether ROHC runs on a given DRB, and with which profiles, is decided by RRC at bearer setup and can differ per DRB.
Configured by RRC: the PDCP-Config for a DRB carries a headerCompression choice — either notUsed or rohc. When rohc is chosen it carries maxCID (the highest context identifier the peer may use) and a set of boolean flags naming the enabled profiles (for example profile0x0001, profile0x0002, profile0x0004). Both endpoints must agree on this configuration before compression can start.
Because the configuration is per DRB, a UE can run RTP/UDP/IP compression on its voice bearer while running no compression, or a different profile set, on a parallel data bearer. The maxCID value is shared across the profiles configured on that DRB and bounds how many simultaneous flows (contexts) the compressor may track on it. Note the ordering carefully: because ROHC precedes ciphering on transmit and follows deciphering on receive, a decompressor never sees a compressed header it could not decipher — a ciphering fault surfaces as a PDCP integrity/decode failure, not as a ROHC context failure, and the two are diagnosed separately.
ROHC Profiles
A profile tells ROHC which protocol stack it is compressing, so it knows which fields are static, which are inferable, and which must be sent explicitly. Each profile is identified by a 16-bit number and is enabled through the RRC profiles flags. The compressor may only use a profile the decompressor has advertised support for; profile 0x0000 is always implicitly available as the safe passthrough.
| Profile ID | Name | Compresses | Typical use in LTE |
|---|---|---|---|
0x0000 | Uncompressed | Nothing (passthrough) | Baseline / fallback; carries the packet with only a small ROHC prefix |
0x0001 | RTP/UDP/IP | IP + UDP + RTP | VoLTE voice and real-time media — the workhorse profile |
0x0002 | UDP/IP | IP + UDP (no RTP) | Non-RTP UDP flows (DNS, some signalling, RTCP) |
0x0004 | IP | IP only | Non-UDP/non-TCP IP traffic (e.g. ICMP, other transport) |
0x0006 | TCP/IP | IP + TCP | TCP flows (ROHC-TCP, RFC 6846) |
For VoLTE the workhorse is 0x0001 (RTP/UDP/IP). The RTP sequence number advances by exactly one per packet and the RTP timestamp advances by a fixed codec increment, which is precisely the tight, predictable redundancy ROHC exploits best — once the decompressor knows the pattern, the compressor can often send just a few bits of sequence number and let everything else be inferred. The 0x0002 and 0x0004 profiles handle the flows that lack the full RTP stack, and 0x0006 handles TCP, where compression is trickier because acknowledgements and window updates change less predictably.
The original RTP/UDP/IP, UDP/IP and IP profiles are defined in RFC 3095 and refined by RFC 4815; the more robust "ROHCv2" variants (profiles 0x0101, 0x0102, 0x0104) come from RFC 5225, and ROHC-TCP (0x0006) from RFC 6846. The 16-bit profile identifier is what both ends exchange so that compressor and decompressor apply exactly the same field model — using different profiles on the two ends would desynchronise them immediately.
Note: a DRB usually has several profiles enabled at once. ROHC inspects each incoming packet, classifies it, and picks the most specific matching profile — RTP/UDP/IP if it is a full RTP flow, UDP/IP if it is bare UDP, and so on — assigning each distinct flow its own context.
Static and Dynamic Field Classification
ROHC's entire saving comes from sorting every header field into one of a few behavioural classes, and then transmitting each class only as often as it actually needs. The profile defines that classification. Broadly:
| Field class | Behaviour | Examples | How ROHC treats it |
|---|---|---|---|
| Static | Constant for the whole flow | Source/dest IP, source/dest port, RTP SSRC, IP version | Sent once in the IR packet, then never again |
| Static-known | Fixed by the profile / standard | IP protocol number, UDP length relationship | Never sent — both ends already know it |
| Inferred | Derivable from other fields | IP total length, UDP checksum presence, IP header checksum | Reconstructed at the decompressor, not transmitted |
| Changing (predictable) | Changes by a fixed delta | RTP sequence number, RTP timestamp, IP-ID | Established in FO, then sent as a tiny delta / W-LSB value |
| Changing (irregular) | Changes unpredictably | RTP marker bit, occasional TS jumps | Sent explicitly whenever it changes |
The static chain (the fields that never change) is carried once in the IR packet and thereafter implied purely by the CID. The dynamic chain (the fields that change) is what the compressor tracks and encodes. The saving is greatest for RTP/UDP/IP because so much of an RTP header falls into the "static" or "inferred" buckets, leaving essentially one counter — the RTP sequence number — to drive everything else.
Packet Types and W-LSB Encoding
The states below decide how small a header can be; the ROHC packet types are the concrete formats that carry each size. It is worth naming them because a packet capture and most vendor counters are labelled by packet type, not by state.
| Packet type | Carries | Sent in state |
|---|---|---|
IR | Full static chain + dynamic chain (rebuilds/initialises the context) | IR |
IR-DYN | Dynamic chain only (statics already known, refresh the dynamics) | IR / FO |
UO-0 | Smallest header — a few LSBs of sequence number only | SO |
UO-1 | Sequence number plus one changed field (e.g. TS or IP-ID) | SO / FO |
UOR-2 | Larger update carrying multiple changed dynamic fields | FO |
The key mechanism for the predictable fields is W-LSB (window-based least-significant-bits) encoding: instead of sending the full sequence number, the compressor sends only enough low-order bits that the decompressor, which already knows the recent value, can unambiguously reconstruct the rest. The compressor keeps a sliding window of recently sent values and chooses k bits wide enough to disambiguate across an interpretation interval anchored on a reference value; the decompressor uses the same reference and interval to expand them. As long as the two stay roughly in sync, a handful of bits suffices — and the RTP timestamp is then scaled and inferred from the sequence number using the codec's fixed sampling rate (TS-stride). This is why a steady-state VoLTE header can shrink to a single byte: almost everything is static, inferred, or derived from one small counter.
Why a window, not a single value: the radio loses packets, so the decompressor's "last known" sequence number may lag the compressor's. The window makes the LSB count large enough that even after a burst of losses the value still expands unambiguously — robustness is bought with a bit or two more, not with a full retransmission of the header.
Compressor and Decompressor States
ROHC works because both ends hold matching state and move through it in step. Each direction runs its own three-state machine. The compressor climbs from sending everything toward sending almost nothing, gaining confidence that the decompressor is in sync; the decompressor tracks how complete its own context is and drops back when it loses sync.
| Compressor state | Meaning | What it sends |
|---|---|---|
IR (Initialization & Refresh) | Building or repairing the context | The full, uncompressed header including all static fields — also sent periodically to refresh |
FO (First Order) | Static fields known; change pattern being established | Updates for the dynamic fields whose delta pattern is not yet fully locked in |
SO (Second Order) | Steady state; change pattern fully predictable | The smallest possible header — typically just a compressed sequence number |
The compressor is optimistic: it moves upward (toward SO) as soon as it has reasonable confidence the decompressor has received enough packets, and it moves back down toward IR or FO when a timeout expires or the decompressor signals a failure. The three decompressor states mirror this:
- No Context (
NC) — nothing learned yet; the decompressor must wait for anIRpacket before it can decompress anything. - Static Context (
SC) — the static fields are known but dynamic-field synchronisation has been lost; the decompressor can accept anIRorFOpacket to recover. - Full Context (
FC) — everything is known; the decompressor can reconstruct even the smallestSOheaders. It falls back toSConly after repeated failures.
Mental model: the compressor optimistically assumes the decompressor is keeping up and shrinks the header aggressively; the decompressor drops back a rung whenever it loses sync. The operating mode below governs how quickly and reliably the two re-synchronise.
Operating Modes, Feedback and the CID
The same states behave differently depending on the operating mode, which is really a choice about how much feedback the decompressor sends back to keep things robust over a lossy radio link. ROHC defines three modes and can transition between them during a flow.
| Mode | Feedback | Behaviour |
|---|---|---|
U-mode (Unidirectional) | None | Compressor sends with no return channel; relies on periodic IR/refresh timeouts to stay robust. Every flow starts here, and it is the only option on a truly one-way link. |
O-mode (Bidirectional Optimistic) | Occasional (errors only) | Decompressor sends feedback mainly to signal failures (NACK/STATIC-NACK). A good balance of overhead and robustness — the common working mode for VoLTE. |
R-mode (Bidirectional Reliable) | Frequent (ACK/NACK) | Most robust; context-changing updates are acknowledged, minimising context loss at the cost of more feedback traffic. |
Feedback travels in the reverse direction as a small ROHC feedback packet: an ACK confirms the decompressor is in Full Context, a NACK asks the compressor to drop to FO and resend dynamic fields, and a STATIC-NACK reports total context loss, forcing the compressor back to IR with the full header. This feedback loop is what lets the compressor stay in SO confidently even when the odd packet is lost to radio conditions.
Every flow ROHC compresses gets its own context, addressed by a Context Identifier (CID). The compressed packet carries the CID so the decompressor applies the right state. The maxCID value from RRC caps how many concurrent contexts the compressor may run on the DRB and selects the CID encoding: small-CID (values 0–15, encoded in a single byte or an add-CID octet) when maxCID ≤ 15, or large-CID (a variable-length self-describing field, values up to 16383) when more contexts are needed. A small maxCID means fewer simultaneous compressed flows but cheaper CID encoding — for a voice bearer carrying one media flow plus perhaps RTCP, a very small maxCID is plenty.
VoLTE Capacity and Context at Handover
The header saving is not a nicety — it is central to whether VoLTE is economically viable at all. Consider the arithmetic. Without ROHC, each 20 ms voice packet needs a 40-byte header wrapped around a ~32-byte sample, so more than half of every scheduled voice transmission is overhead. With ROHC in steady state the header is one to three bytes, so nearly all of the transmission is payload. That directly multiplies the number of VoLTE users a cell can carry, because the scheduler is spending its precious resource blocks on voice rather than on repeated IP/UDP/RTP boilerplate. The saving also shrinks the transport-block size, which lets the codec frame fit into a more robust, lower-order modulation and coding scheme — improving cell-edge voice coverage at the same time.
Why VoLTE depends on it: a ~60‑byte header collapsing to ~1‑3 bytes is the difference between voice payload and header being comparable in size, and voice payload dominating. Combined with semi-persistent scheduling and RLC UM, ROHC is what makes packet voice competitive with legacy circuit-switched capacity.
All of that saving depends on the compressor and decompressor holding a shared context — which raises an obvious question at handover. When the UE moves to a new eNB, the PDCP entity is normally re-established, and a re-established ROHC entity resets to No Context, forcing a fresh round of IR packets with full headers just when the radio link is least reliable. To avoid that hit, RRC can set drb-ContinueROHC in the PDCP-Config carried in the handover command. When present, it tells the UE not to reset the ROHC context but to carry it forward into the target cell, so compression continues in SO/Full Context without rebuilding.
drb-ContinueROHC: used on intra-system handovers where the same header-compression context remains valid at the target — typically an intra-eNB handover, or an inter-eNB handover where the network keeps the ROHC state. Its presence in the reconfiguration signals "keep the context"; its absence means the UE re-establishes PDCP and ROHC restarts from IR. Getting this right avoids a burst of full-size headers exactly at the fragile handover moment.
Note that ciphering keys always change at handover, but header compression and ciphering are independent PDCP steps — continuing the ROHC context does not conflict with re-keying, because ROHC operates on the plaintext before encryption. The two decisions are made separately.
LTE ↔ NR: NR PDCP (TS 38.323) keeps the very same ROHC framework, profiles and states — ROHC is configured in the NR PDCP-Config exactly as in LTE, and drb-ContinueROHC survives into NR reconfiguration. The big addition in NR is a second, NR-specific compressor alongside ROHC: EHC (Ethernet Header Compression, RFC-independent, 3GPP-defined) for Ethernet PDU-session traffic, plus UDC (Uplink Data Compression) for payload. ROHC concepts you learn here transfer directly; NR simply adds companions for traffic types LTE never carried on the user plane.
Summary
ROHC is a PDCP user-plane function that turns a 40–60 byte IP/UDP/RTP header into one to three bytes by exploiting the fact that almost every header field is static, inferable, or changes by a predictable delta. It runs on DRBs only (never SRBs), is turned on per DRB by the RRC PDCP-Config with a chosen maxCID and a set of profiles, and operates on the plaintext before ciphering.
The engine is a shared context: the compressor sends a full IR header once, climbs IR→FO→SO as confidence grows, and thereafter emits tiny UO-0/UO-1 packets carrying only a W-LSB-encoded sequence number. Feedback (ACK/NACK/STATIC-NACK) and the operating mode (U/O/R) keep the two ends in step over a lossy link, and drb-ContinueROHC preserves the context across handover so the saving is not thrown away at the worst moment. For VoLTE, that saving is what makes packet voice competitive with legacy circuit-switched capacity.
⚠ Common pitfalls / gotchas
- Profile mismatch between ends. If the two sides enable different
profilessets, a flow that one end compresses under (say) RTP/UDP/IP cannot be decompressed by a peer that only advertised UDP/IP — the decompressor stays in No Context and headers never shrink. - maxCID too small for the flow count. Sizing
maxCIDbelow the number of concurrent flows on the DRB forces contexts to be evicted and re-initialised, so packets keep falling back to fullIRheaders and the compression gain collapses. - Forgetting
drb-ContinueROHCat handover. Omitting it re-establishes PDCP and resets ROHC to No Context, producing a burst of full-size headers exactly when the target radio link is most fragile. - Blaming ROHC for a ciphering fault. Because ROHC runs on plaintext before ciphering, a mis-keyed or corrupted PDCP payload shows up as a decode/integrity failure, not a context failure — chasing it as a ROHC sync problem wastes time.
- Assuming U-mode is enough on a two-way link. Without feedback, the only recovery from context loss is the periodic
IRrefresh timer; on a lossy link O-mode (NACK-driven) recovers far faster and is usually the right VoLTE choice.
Q. Why is ROHC applied only to DRBs and never to SRBs?
A. ROHC exploits redundancy in IP/UDP/RTP (or IP/UDP, IP, TCP) user-plane headers. SRBs carry RRC/NAS signalling, which has no such stack to compress and must stay maximally reliable, so ROHC is a user-plane, DRB-only function configured per DRB in PDCP-Config.
Q. What is the difference between the IR, FO and SO compressor states?
A. IR sends the full header to build or refresh the context; FO sends dynamic-field updates while the change pattern is being established; SO is steady state, sending the smallest header (usually just a compressed sequence number). The compressor climbs IR→FO→SO as confidence grows and drops back on timeout or NACK.
Q. What is a CID and how is its range bounded?
A. A CID (Context Identifier) labels the per-flow ROHC context so each compressed packet maps to the right state. RRC sets maxCID, which caps the number of concurrent contexts on the DRB and selects small-CID (≤15) versus large-CID encoding.
Q. Which profile does VoLTE use, and why that one?
A. Profile 0x0001 (RTP/UDP/IP). The RTP sequence number and timestamp advance in a tight, predictable pattern, so after the context is built the compressor can send just a few sequence-number bits and infer the rest — collapsing a ~40-byte header to about one byte.
Q. What does drb-ContinueROHC do at handover?
A. It tells the UE to carry the existing ROHC context into the target cell instead of resetting it, so compression stays in Full Context/SO rather than restarting from IR with full headers — avoiding a burst of overhead at the fragile handover moment.
Where ROHC connects
ROHC is a PDCP user-plane function whose biggest payoff is voice, so it sits right next to the bearer stack and the VoLTE flow that depends on it.