A frame without a capture time is a frame you cannot defend in a timing argument: you know when your code got it, not when the sensor saw the part. Linux already answers this. Every buffer the V4L2 subsystem hands back carries a timestamp filled in by the driver when the frame was captured, plus flags that name the clock it came from. Most applications ignore both and stamp the dequeue moment instead. That adds the entire queue depth — tens of milliseconds — of unknown age to every frame. This how-to shows you how to read the stamp, identify its clock, map it to your log time and prove it with a lag test, so a part moving at 0.5 m/s lands in your measurement within a millisecond of where it really was.
What the kernel stamps, and in which clock
Each dequeued v4l2_buffer carries a timestamp field — a struct timeval with tv_sec and tv_usec — that the driver fills when the frame's data is complete, not when you dequeue it. The flags in the same structure name the clock. V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC means CLOCK_MONOTONIC: seconds since boot, immune to NTP steps, directly comparable with clock_gettime() on your side. V4L2_BUF_FLAG_TIMESTAMP_COPY means the value was copied from another clock domain and has an arbitrary offset, so never compare it with host time. Neither flag set means an older driver with an unspecified clock; UVC drivers from the 2.6 era stamped wall-clock time. Some drivers also say whether the stamp was taken at the start of exposure (V4L2_BUF_FLAG_TSTAMP_SRC_SOE) or at the end of the frame (V4L2_BUF_FLAG_TSTAMP_SRC_EOF), which matters once exposures run long. Your kernel's V4L2 userspace API documentation has the full buffer flags table.
The queue side of the delay — frames waiting in the driver while your loop falls behind — is covered in Camera buffer timestamp latency: interpret frame timing.
Read the capture time in six steps
- Zero the v4l2_buffer before each VIDIOC_QBUF (buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE, buf.memory = V4L2_MEMORY_MMAP for mmap streaming) so flags from a previous buffer cannot leak into the next one.
- Dequeue with VIDIOC_DQBUF, then call clock_gettime(CLOCK_MONOTONIC) immediately. Those two values are the whole measurement: the driver's capture time and your dequeue time.
- Read buf.timestamp.tv_sec and buf.timestamp.tv_usec as seconds, and check buf.flags for V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC before subtracting anything.
- Compute frame_age = t_dequeue − t_capture per frame and log it per frame; the average hides the one frame that breaks your measurement.
- Sample CLOCK_REALTIME and CLOCK_MONOTONIC once at start-up, store offset = realtime − monotonic, and stamp log entries with t_capture + offset.
- Log buf.sequence next to the stamp, so a dropped frame shows up as a sequence jump instead of a silent gap.
The code, with the driver's names and the derived variables marked:
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <time.h>
struct v4l2_buffer buf; /* driver struct: zero before every QBUF/DQBUF */
struct timespec now; /* dequeue time, CLOCK_MONOTONIC */
double stamp_s, now_s, age_ms; /* derived: seconds, then ms */
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) { /* EAGAIN when no buffer is ready */ }
clock_gettime(CLOCK_MONOTONIC, &now);
stamp_s = buf.timestamp.tv_sec + buf.timestamp.tv_usec / 1e6;
now_s = now.tv_sec + now.tv_nsec / 1e9;
age_ms = (now_s - stamp_s) * 1000.0;
/* V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC in buf.flags says the clock is CLOCK_MONOTONIC */
The test that proves the code is honest: run the loop against a 60 fps UVC camera with a 100 ms sleep added per iteration. Correct stamps read age_ms between about 83 and 100 ms — the buffer you requeue after dequeue is claimed by the next frame within one 16.7 ms frame period, then waits out the rest of your loop — and buf.sequence jumps by about six per iteration, since six frames at 60 fps fit in 100 ms. If age_ms reads near zero, you are stamping dequeue time. If it drifts steadily, the two clocks in the subtraction are not the same clock.
What dequeue time costs: a worked example
Setup: a part on a belt at 0.5 m/s, filmed by a 60 fps UVC camera, four buffers queued, a processing loop that takes 100 ms per frame.
- Frame period: 1/60 s = 16.7 ms.
- Queue bound: 4 × 16.7 ms = 67 ms of worst-case wait inside the driver.
- Loop bound: a 100 ms iteration is slower than the queue, so the worst case is about 100 ms — exactly what the lag test reads (83-100 ms).
- Throughput: 1000 ms / 100 ms = 10 frames processed per second, so 60 − 10 = 50 frames are dropped each second.
Stamping at dequeue puts the part 0.5 m/s × 0.100 s = 0.050 m = 50 mm away from where you log it. Using buf.timestamp leaves only the exposure-to-stamp residual: 0.5 m/s × 0.002 s = 0.001 m = 1 mm at a typical 2 ms, and the strobe test below gives you your own residual. When the loop is only slightly slower than the camera — 20 ms against 16.7 ms — the queue bound wins:
Map the monotonic stamp to wall-clock time
CLOCK_MONOTONIC has no timezone and no NTP steps — ideal for ages and intervals, useless for log files. Bridge the two once, at start-up: call clock_gettime(CLOCK_REALTIME) and clock_gettime(CLOCK_MONOTONIC) back to back, store offset = realtime − monotonic, and add it to every stamp you write to disk. CLOCK_REALTIME is allowed to step when NTP corrects it, so resample the offset whenever two paired samples disagree by more than about a millisecond, or when you see the wall clock jump. Frame-to-frame intervals, belt positions and every subtraction between two frames stay in monotonic time. The same mapping problem at the camera-clock level, including rollover, is the subject of Camera timestamp clock rollover and monotonic time mapping.
Three checks before you trust the stamp
- Interval check. Subtract consecutive stamps. At 60 fps the deltas should sit at 16.7 ms with sub-millisecond jitter; deltas of 33.3 ms mean whole frames are being dropped, and deltas that wander mean the USB bus cannot keep up with the format you chose.
- Lag test. The 100 ms-sleep run from the steps above: age_ms in the 83-100 ms range proves the stamp is the driver's, not yours.
- Strobe test. Flash an LED or toggle a GPIO line at a known CLOCK_MONOTONIC instant in front of the lens, find the event in the frame, and subtract. The difference is your exposure-to-stamp residual, typically 1-3 ms on UVC: exposure tail, readout start and up to a millisecond of USB frame quantization all sit in front of the stamp. Under auto exposure the residual moves with the exposure time, so pin the exposure down — and if V4L2 exposure controls appear to be ignored, that has its own diagnosis in V4L2 exposure and gain settings ignored by a sensor driver. To see the real exposure per frame, read it back from the metadata as in Read exposure time from frame metadata under auto exposure.
One measuring note: benchmark the loop period with wall-clock time, not CPU time — a loop that blocks on VIDIOC_DQBUF spends its life outside CPU time (CPU time vs wall-clock time in vision pipeline benchmarking).
When a stamp computed on the host is not enough
Four cases where the buffer stamp still is not the whole answer:
- Rolling shutter. One stamp describes one instant, but a rolling-shutter frame is exposed row by row; at 30 fps the scan can run across most of the 33.3 ms frame period, so a fast part is captured at many positions the single stamp cannot name. A global shutter gives every frame one true exposure instant.
- COPY flags and legacy drivers. Intervals between frames of the same stream remain usable, but cross-clock comparisons are not.
- Frameworks that hide the buffer. If you capture through a wrapper that hands you decoded frames, everything you stamp is dequeue time; stay at the V4L2 level for timing work, or wrap the camera's own frame buffer into your pipeline (Wrap an SDK frame buffer in an OpenCV cv::Mat without copying).
- Exposure-instant accuracy. Industrial cameras built on GigE Vision, USB3 Vision and GenICam can latch the timestamp at exposure end and deliver it with the image as chunk data; on that path the queue lag this article fights simply does not exist. How such a stamp works is covered in Chunk data timestamp: camera time at exposure end and how to latch it.
That last class is what to move to when the timing budget is tight: a global-shutter board-level camera for compact embedded housings, or a C-mount area scan camera when optics matter as much as timing.
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 5 MP
- Sensor format
- 1/2.9"
- Lens mount
- M12
- Interface
- GigE
- Resolution
- Up to 3 MP
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Frame rates set the scale of the problem: at 60 fps one queued frame is 16.7 ms old before you touch it, and at 120 fps it is 8.3 ms (1/120 s), so even a shallow queue costs a measurable belt shift — which is exactly what a stamp latched inside the camera removes.
Common mistakes that skew timestamps
- Stamping after decode or color conversion: by the time your image library hands you a frame, the buffer stamp is long gone. Log it at dequeue, before any conversion.
- Reading tv_sec as Unix epoch: under V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC it counts seconds since boot.
- Comparing a V4L2_BUF_FLAG_TIMESTAMP_COPY stamp with host clocks: arbitrary offset, garbage ages.
- Growing the queue to stop drops: every extra buffer adds another frame period to the worst-case age, and the honest fix is a faster loop (Acquisition faster than processing: fix the frame rate gap).
- Trusting the stamp across a driver change: an update can change the clock domain, so re-run the interval and strobe checks after any driver or kernel move.
The decision
Read buf.timestamp on every frame. Trust it when V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC is set, bridge it to wall clock once with a paired clock_gettime() sample, and budget worst-case age as max(loop_period, queue_depth × frame_period). If that budget exceeds what your line tolerates, shrink the queue and the loop first; if you need the exposure instant itself — robot guidance, event timing, any number you quote to a customer — move to a camera that latches the stamp at exposure end. Compare models in the camera finder, spec a full setup in the configurator, or keep reading on the systems topic page; the contact page reaches us for a specific build, and free expert support comes with every order.
Questions
How do I check buffer flags and timing without writing code?
Use v4l2-ctl from the v4l-utils package. The command v4l2-ctl -d /dev/video0 --stream-mmap=4 --stream-count=10 --verbose streams ten frames and prints per-buffer delivery timing, and --all shows the driver, pixel format and frame rate your stamps will come from. It does not print the timestamp field itself, but it confirms format and drop behaviour in seconds before you read stamps in your own code.
Why does my frame age come out negative or jump backwards?
Because the subtraction mixes clocks. A monotonic buffer stamp against a CLOCK_REALTIME log time is valid only while the NTP-corrected offset between them stays fixed; when it steps, old frames look like they were captured after you processed them. Take one paired sample of both clocks at start-up, apply the offset as a constant, and resample the pair whenever it moves by more than a millisecond.
Does the v4l2 timestamp include the exposure time?
No. It marks when the frame's data was complete at the driver — for UVC, roughly when the first USB packets of the video frame reached the host — so exposure and sensor readout sit in front of it. A 1 ms exposure adds little; a 30 ms low-light exposure pushes the stamp most of a frame period after the light arrived. Measure your residual once with a strobed LED, and if the stamp must coincide with the exposure itself, use a camera that latches chunk timestamps at exposure end.
How many buffers should I queue with VIDIOC_REQBUFS?
The fewest that never starve the loop: three is a sound starting point. At 60 fps each queued frame is 16.7 ms of potential age, so a shallow queue bounds the wait near 33 ms and four buffers near 67 ms. Watch buf.sequence as you tune: gaps with a shallow queue mean processing must speed up, because every added buffer turns a fast drop into slow latency that only the timestamp can uncover.
Can I compare these stamps across two cameras?
Within limits. Two cameras on the same host that set V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC share CLOCK_MONOTONIC, so their stamps subtract cleanly, but each carries its own exposure-to-wire residual of a few milliseconds. Sub-millisecond agreement between cameras needs hardware triggering or cameras that share one clock. Across two hosts the monotonic clocks are unrelated, so bridge both sides to a common reference such as PTP before comparing anything.


