Internet Protocols · Real-Time Media
The RTP Fixed Header: Twelve Bytes That Carry a Media Stream
What each field of the 12-byte RTP header means, why sequence numbers and timestamps are independent, and which failures the design accepts to stay small.
A media stream sent over UDP has no connections, no acknowledgements, and no retransmission timer. The receiver still has to answer three questions for every packet: where does this packet belong in the stream, when should its content be played, and which stream does it belong to at all. RTP answers all three in a fixed header of twelve bytes, and every other decision in the protocol follows from that budget.
The header is defined in RFC 3550 and is the same for audio, video, and text. Payload formats layered on top of it add their own descriptors, but the twelve bytes below are invariant.
Field by field, from the wire upward
The first byte carries the version in its top two bits, currently 2. A padding flag marks that the last octet of the packet is a count of trailing padding bytes, which block ciphers and some encryption profiles need. An extension flag says a header extension follows the fixed header. The low four bits are the CSRC count, the number of contributing source identifiers that trail the header.
The second byte holds a single marker bit and a seven-bit payload type. The payload type is an index into a table negotiated out of band, normally by SDP. RFC 3551 fixes a handful of static assignments, such as PCMU at 0 and G.722 at 9, and reserves the dynamic range 96 through 127 for formats whose parameters are described in the session description. A receiver that sees an unknown payload type cannot decode the packet; the payload type is not self-describing.
Then come three 32-bit fields. The sequence number increments by one per packet, starting from a random value. The timestamp gives the sampling instant of the first octet in the payload. The synchronization source identifier, SSRC, is a 32-bit value chosen at random when the stream begins and never reused within a session.
Contributing source identifiers follow, four bytes each, and identify sources whose content has been mixed into this packet, as in an audio bridge. A mixer that combines four speakers lists four CSRCs and keeps its own SSRC as the packet’s origin. A receiver displaying talker identity reads the CSRC list; a receiver decoding audio ignores it entirely. This is why the count field is four bits: the practical limit for a mixer is a small conference, and fifteen contributors fit without an extension.
Header extensions, when the X bit is set, begin with a 16-bit profile identifier and a 16-bit length in 32-bit words, followed by the extension data. RFC 8285 later defined one-byte and two-byte extension element formats that pack multiple small extensions into that space, which is how modern stacks carry send timestamps, transport-wide sequence numbers, and audio levels without spending a full 32-bit word per value.
Sequence numbers and timestamps answer different questions
The two counters look redundant until a packet is lost or delayed. The sequence number is about delivery: it lets a receiver detect loss, measure reordering, and place packets in order. The timestamp is about content: it says when the samples inside the packet were captured, measured in units of the codec’s clock.
They diverge constantly. A 20 ms audio packet carries one sequence number and a timestamp advance of 960 at a 48 kHz clock. A video frame split across eight packets carries eight sequence numbers and one shared timestamp, because all eight packets describe the same instant. A sender using silence suppression emits nothing for a quiet period and then jumps the sequence number forward by however many packets it would have sent, while the timestamp jumps by the corresponding number of sample periods. Neither field alone can reconstruct the other.
Both counters wrap. The sequence number is 16 bits, so it wraps after 65,536 packets; at 50 packets per second that is roughly 22 minutes. The timestamp is 32 bits and wraps according to the clock rate, covered in detail in why RTP timestamps do not count seconds. Comparisons must therefore be modular, not ordinary integer comparisons: a packet with sequence number 3 arriving after 65,533 is new, not 65,530 packets late.
The initial values are randomized for a reason that has nothing to do with security and everything to do with middleboxes. A stream that always started at sequence 1 and timestamp 0 would be trivially spoofable by an off-path attacker who guessed the next values, and would also let an observer correlate two sessions as coming from the same sender. RFC 3550 requires the initial sequence number and timestamp to be random, and requires the timestamp offset to be chosen once per stream rather than per packet so that the differences between consecutive timestamps remain meaningful.
The SSRC has a similar rule with a harder consequence: two sources in the same session that pick the same SSRC collide, and receivers cannot tell their packets apart. RFC 3550 defines collision detection through RTCP, where each participant announces its SSRC, and resolution by generating a new identifier. Because detection depends on periodic reports rather than on the data path, a collision can persist for the report interval, and during that interval the receiver’s jitter and loss statistics are silently corrupted.
What the design deliberately leaves out
The header has no length field, because UDP already carries the datagram length. It has no checksum of its own, because UDP’s checksum covers the payload and header together on IPv4 and is mandatory on IPv6. It has no connection identifier, because the SSRC plays that role and a session can carry many streams. It has no acknowledgement field, because RTP is not a reliable protocol and the feedback that does exist lives in a separate protocol, described in RTCP sender and receiver reports.
The cost of this minimalism is that a receiver cannot verify much on its own. A packet with a valid header and an unknown SSRC is indistinguishable from a legitimate stream that has just started, unless the receiver has seen an announcement. A packet with a plausible sequence number and a corrupted payload will be decoded into noise, because nothing in RTP detects bit errors. The protocol assumes the transport detects corruption and the application tolerates loss.
Twelve bytes is also a deliberate ceiling for header compression. On a low-bandwidth link, a 20 ms G.729 packet carries 20 bytes of payload; adding 12 bytes of RTP, 8 of UDP, and 20 of IPv4 costs 40 bytes of header for 20 bytes of speech, a 66 percent overhead. RTP header compression exists precisely because that ratio is intolerable on slow links, and it works by exploiting the predictability of every field except the sequence number and timestamp. A design that added optional per-packet fields would have made that compression impossible.
Parsing a header without trusting it
The fixed header is easy to read and easy to misread. The following C function reads the fields a receiver needs first, and rejects packets whose declared structure does not fit inside the datagram.
#include <stdint.h>
#include <stddef.h>
struct rtp_hdr {
uint8_t version;
int padding;
int extension;
uint8_t csrc_count;
int marker;
uint8_t payload_type;
uint16_t seq;
uint32_t timestamp;
uint32_t ssrc;
size_t header_len; /* fixed + CSRCs + extension, in bytes */
};
/* Returns 0 on success, -1 if the declared structure exceeds len. */
int parse_rtp(const uint8_t *buf, size_t len, struct rtp_hdr *out)
{
if (len < 12)
return -1;
out->version = buf[0] >> 6;
out->padding = (buf[0] >> 5) & 1;
out->extension = (buf[0] >> 4) & 1;
out->csrc_count = buf[0] & 0x0f;
out->marker = buf[1] >> 7;
out->payload_type = buf[1] & 0x7f;
out->seq = (uint16_t)(buf[2] << 8 | buf[3]);
out->timestamp = (uint32_t)buf[4] << 24 | (uint32_t)buf[5] << 16 |
(uint32_t)buf[6] << 8 | buf[7];
out->ssrc = (uint32_t)buf[8] << 24 | (uint32_t)buf[9] << 16 |
(uint32_t)buf[10] << 8 | buf[11];
size_t hlen = 12 + 4u * out->csrc_count;
if (hlen > len)
return -1;
if (out->extension) {
if (hlen + 4 > len)
return -1;
size_t words = (size_t)buf[hlen + 2] << 8 | buf[hlen + 3];
hlen += 4 + 4 * words; /* length counts 32-bit words, not bytes */
if (hlen > len)
return -1;
}
out->header_len = hlen;
return 0;
}
The extension length is the field most often misread: it counts 32-bit words, so a declared length of 3 means 12 bytes of extension data. A parser that treats it as bytes will hand the decoder a payload that begins twelve bytes early.
Failure modes that follow from the format
Because the marker bit means different things per payload type, a receiver that assumes one convention breaks on another. In audio it marks the first packet after a silence period, which the receiver can use to reset its playout logic. In video it marks the last packet of a frame, which tells the receiver that the frame is complete enough to decode. A relay that rewrites the marker bit for its own purposes breaks both interpretations.
Padding is another trap. When the P bit is set, the final octet gives the number of padding octets including itself, so a packet whose last byte is zero is malformed and a packet whose last byte exceeds the remaining length is malformed. Removing padding by stripping the last octet unconditionally corrupts the payload of any packet that happens to end in a zero byte without padding enabled.
The most consequential omission, though, is ordering information beyond the sequence number. RTP gives the receiver a 16-bit window and nothing else; how far out of order a packet may arrive before it is treated as lost is a receiver policy, not a protocol rule. That policy is the jitter buffer, and its sizing is a separate design problem treated in jitter buffer adaptation. The header provides the raw material; the buffer decides what to do with it.
References
- RFC 3550: RTP: A Transport Protocol for Real-Time Applications — IETF
- RFC 3551: RTP Profile for Audio and Video Conferences with Minimal Control — IETF
- RTP: Audio and Video for the Internet — Colin Perkins, Addison-Wesley, 2003