Internet Protocols · Real-Time Media

Why RTP Timestamps Do Not Count Seconds

RTP timestamps advance at the codec clock rate, which makes arithmetic modular and stream-relative; the rules that follow for wrap, comparison, and cross-stream sync.

A receiver that wants to know when to play a packet has two obvious choices for a timestamp: a wall-clock instant, or a count of samples. RTP picks the count of samples, and the consequences of that choice run through every part of the protocol. The field is a 32-bit integer that advances by the number of sample periods the payload spans, at a rate determined by the codec rather than by any convention.

For 8 kHz narrowband speech the clock runs at 8000 ticks per second. For 48 kHz audio it runs at 48000. For video the rate is nominally 90000 regardless of frame rate, a value inherited from MPEG transport streams. RFC 3551 lists the static payload types with their clock rates; dynamic payload types get their rate from the session description, and the rate is part of the format definition rather than something the receiver can infer from the data.

The 90 kHz convention and what it buys

Video at 90000 ticks per second gives 11.1 microseconds per tick. That resolution is finer than any frame interval and divides evenly into the common frame rates in a way that a power-of-two rate would not: 3000 ticks per frame at 30 fps, 3600 at 25 fps, 3750 at 24 fps. Integer arithmetic on frame durations stays exact, so a sender computing the timestamp for frame n can do it with multiplication rather than floating point, and a receiver comparing timestamps can compare integers.

The choice also matches MPEG-2 systems, where presentation timestamps use a 90 kHz clock derived from a 27 MHz system clock divided by 300. Reusing the rate meant that gateways between RTP and MPEG streams could pass timestamps through without rescaling, which was a significant simplification when the protocol was designed for interoperability with the broadcast world.

A non-integer frame rate breaks the exactness but not the design. At 29.97 fps, which is 30000/1001, a frame lasts 3003 ticks, and a sender that rounds each frame independently accumulates drift of up to half a tick per frame. Over an hour at 30 fps that is 108000 frames and a worst-case error of tens of milliseconds if rounding always went the same way. The standard fix is to compute the timestamp for frame n directly from the stream’s start timestamp as start + round(n * 90000 * 1001 / 30000) rather than to add a fixed increment per frame, which keeps the error bounded by half a tick indefinitely.

Audio has no such freedom. The timestamp must advance by exactly the number of samples the payload contains, or the receiver’s notion of playback time diverges from the content. A 20 ms Opus frame at 48 kHz carries 960 samples and the timestamp advances by 960. A sender that loses an audio frame and simply skips it without advancing the timestamp by 960 has shifted all subsequent audio 20 ms earlier in the stream’s timeline, and the receiver has no way to detect it.

Modular arithmetic is mandatory

The timestamp is 32 bits, so it wraps. At 48 kHz the wrap period is 2^32 / 48000 seconds, about 24.85 hours. At 90 kHz it is about 13.25 hours. A session longer than that wraps at least once, and any comparison written as if (a > b) breaks at the boundary.

The correct comparison treats timestamps as points on a circle and asks which one is closer in the forward direction. RFC 3550 defines the relationship using signed 32-bit differences:

#include <stdint.h>

/* True if a is "later" than b, assuming the two are less than 2^31 ticks apart. */
static int ts_after(uint32_t a, uint32_t b)
{
    return (int32_t)(a - b) > 0;
}

/* Forward distance from b to a, in ticks, valid under the same assumption. */
static uint32_t ts_diff(uint32_t a, uint32_t b)
{
    return a - b;
}

Both functions rely on unsigned wraparound being defined, which it is in C, and on the cast to signed reinterpretation being the intended behaviour, which it is on every platform that matters but is formally implementation-defined. The assumption in the comments is the real constraint: if two timestamps are more than 2^31 ticks apart, the sign is ambiguous. At 48 kHz that is 12.4 hours; at 90 kHz, 6.6 hours. A receiver that has not seen a packet from a source for longer than that cannot tell whether the next packet is late or the timestamp has wrapped twice.

Sequence numbers have the same structure at a smaller scale, and the same rule applies: 16 bits, wrap after 65536 packets, compare via signed difference of the 16-bit values.

Why the start value is random and per-stream

The initial timestamp is chosen at random. The reason is partly the same as for sequence numbers, but there is a second reason specific to timestamps: RTP timestamps are defined relative to a per-stream random offset, so two streams in the same session have unrelated timestamp bases even when they are synchronized in reality. That is what forces the NTP correspondence in sender reports, and it is deliberate.

If timestamps were absolute, a receiver could synchronize two streams by comparing their timestamp fields directly. That sounds simpler, and it fails in practice because the streams may have different clock rates: comparing an 8 kHz audio timestamp with a 90 kHz video timestamp requires rescaling, which reintroduces the drift problem. Worse, absolute timestamps would leak the sender’s wall clock into the media stream, which has privacy implications and lets an observer correlate streams from the same host.

The cost of the random base is that the receiver must wait for a sender report before it can place the stream on a common timeline. Until the first report arrives, the receiver can play the stream internally consistently but cannot align it with another stream. For audio and video sent together, this means lip sync cannot be established for the first report interval, typically a few seconds. Implementations bridge the gap by starting video slightly late rather than by guessing.

Clock rate mismatches between sender and receiver

The clock rate in the payload type is a nominal rate, not a measurement. A sender whose audio hardware runs at 48000.3 Hz still stamps packets assuming exactly 48000, because the format says so. A receiver that resamples for playback must reconcile the content’s nominal rate with its own device’s actual rate, and the two drift apart.

The symptom is buffer creep: over a long call, the receiver’s playout buffer fills or drains because it is consuming samples at a slightly different rate than they arrive. The magnitude is small, tens of parts per million on consumer hardware, but over an hour it accumulates. The standard remedies are to resample by the estimated ratio, or to insert or remove a frame occasionally, or to adjust the playout rate slightly. All of them are receiver-side adaptations; RTP itself carries no rate information beyond the nominal clock rate, so the receiver cannot distinguish “the sender’s clock is fast” from “the network delivered a burst.”

Some formats carry explicit timing information that helps. Opus packets can encode a duration in the payload, and the RTP payload format in RFC 7587 constrains how the timestamp advances relative to that duration, so a mismatch between declared duration and timestamp step is a detectable error rather than a silent one. Formats without in-band duration leave the receiver to infer duration from the timestamp step alone, which is correct only if the sender is behaving.

Playout decisions built on the timestamp

A receiver converts timestamps to a playout schedule by maintaining a mapping between RTP time and local time, established from the arrival of the first packets and corrected as later packets arrive. The scheduler then asks, for each packet, whether its scheduled playout instant has passed. That comparison is where the modular arithmetic above matters, and it is also where the jitter buffer enters: the scheduled instant is deliberately set in the past by the buffer’s depth, so that packets arriving early are held and packets arriving within the tolerance are still playable. How that depth is chosen and adjusted is covered in jitter buffer adaptation.

One subtlety in that mapping is that the timestamp marks the sampling instant of the first octet, not the time the content should be audible. For audio, the packet’s content spans the interval from its timestamp to the timestamp of the next packet; the receiver must play it at the start of that interval, not at its end. A receiver that schedules playback at timestamp + duration adds one frame of latency to every packet, which is inaudible on its own but compounds with every stage that makes the same off-by-one assumption.

The same field is what ties the fragments of a large frame together: they all carry one timestamp because they describe one instant, as described in fragmenting encoded frames to fit the path MTU. And when two streams must be aligned on a common timeline, the per-stream random base is what forces the correspondence through sender reports, covered in mapping RTP clocks to wall time.

References

  1. RFC 3550: RTP: A Transport Protocol for Real-Time Applications — IETF
  2. RFC 3551: RTP Profile for Audio and Video Conferences with Minimal Control — IETF
  3. RFC 7587: RTP Payload Format for Opus Speech and Audio Codec — IETF