Quick answer
Video at 29.97 frames per second doesn't fit neatly into a clock that counts 30 frames per second. Non-drop frame (NDF) timecode counts 30 labels per second anyway, so after an hour of video the timecode reads 01:00:00:00 when roughly 1 hour and 3.6 seconds has actually passed. Drop frame (DF) timecode fixes this by skipping certain frame numbers (never actual frames) so the label stays in step with real time.
If you convert timecode to subtitle timestamps and pick the wrong one, your captions drift: perfect at the start, noticeably late by the end.
| Dimension | Drop frame (DF) | Non-drop frame (NDF) |
|---|---|---|
| Frame rates | 29.97 and 59.94 fps | Any rate, including 24, 25, 30, 23.976, 29.97 |
| Frames actually removed | None | None |
| Frame numbers skipped | Yes, at most minute boundaries | No |
| Matches wall-clock time | Yes, very closely | Only at integer frame rates |
| Drift at 29.97 fps | Negligible | About 3.6 seconds per hour |
| Typical notation | 01:00:00;00 (semicolon) | 01:00:00:00 (colons) |
| Common in | US broadcast, program-length timing | Film, 24/25 fps work, many edit sessions |
| Math to convert | More complex | Simple counting |
What SMPTE timecode is
SMPTE timecode (named after the Society of Motion Picture and Television Engineers) gives every frame of video a unique address in the form hours:minutes:seconds:frames. At 25 fps the frame field runs 00 to 24; at 30 fps (or 29.97) it runs 00 to 29.
Timecode is a label, not a measurement. That distinction is the whole story. As long as the frame rate is a whole number, labels and real time agree. When the frame rate is 29.97, they can't, and you have to choose which one to keep honest.
Why 29.97 fps exists
Original black-and-white US television ran at 30 frames per second. When color was added to the NTSC standard in the early 1950s, the color information was carried on a subcarrier, and engineers needed it to avoid interfering with the audio carrier while staying compatible with existing black-and-white sets. The solution was to lower the frame rate very slightly, by a factor of 1000/1001.
So "30 fps" became 30000/1001, about 29.97 fps. The same factor gives the other "odd" rates you'll see:
| Nominal rate | Actual rate | Exact value | Where you see it |
|---|---|---|---|
| 24 | 23.976 | 24000/1001 | Film-style content for NTSC regions, a lot of web video |
| 30 | 29.97 | 30000/1001 | US broadcast, many cameras and phones in NTSC regions |
| 60 | 59.94 | 60000/1001 | Sports, high-frame-rate broadcast |
| 25 | 25 | 25 | PAL regions (Europe and others); no drift issue |
Rates like 25, 50, 24 and 30 are whole numbers, so their timecode never drifts. The headache is specific to the 1000/1001 family.
Where the drift comes from
Non-drop frame timecode counts 30 labels per "second." But only about 29.97 frames arrive per real second. So each timecode second is slightly longer than a real second.
- Per real hour, 29.97 fps video produces about 107,892 frames.
- NDF timecode needs 108,000 frames to reach 01:00:00:00.
- The difference is 108 frames, about 3.6 seconds.
Run a two-hour film at 29.97 NDF and the timecode is roughly 7 seconds behind the clock by the end. For edit decisions that doesn't matter. For broadcast schedules, program durations and captions synchronized to real time, it does.
How drop frame timecode works
Drop frame skips 108 frame numbers per hour to cancel the drift exactly (well, almost exactly; the leftover error is a tiny fraction of a frame per day). It does it with a fixed rule:
- At the start of every minute, skip frame numbers 00 and 01.
- Except every tenth minute (00, 10, 20, 30, 40, 50), where nothing is skipped.
That is 2 numbers skipped in 9 out of every 10 minutes: 18 per 10 minutes, 108 per hour. In practice, the counter jumps like this:
01:00:59;28
01:00:59;29
01:01:00;02 <- ;00 and ;01 do not exist in this minute
01:01:00;03
01:09:59;29
01:10:00;00 <- tenth minute, no skip
01:10:00;01
So a DF timecode like 01:01:00;00 is invalid. It never exists. If your software ever produces it, something has mislabeled NDF as DF.
At 59.94 fps, drop frame works the same way but skips four frame numbers (00 to 03) at each non-tenth minute.
Notation: semicolon vs colon
The usual convention:
- Drop frame: semicolon before the frames, 01:00:00;00. Some tools use a semicolon or period throughout, such as 01;00;00;00 or 01.00.00.00.
- Non-drop frame: all colons, 01:00:00:00.
Treat the separator as a strong hint, not a guarantee. Files exported from scripts, spreadsheets or caption tools often lose the semicolon. When the stakes are high, check the project's frame rate and timecode mode in the editing or caption software directly.
Which one should you use?
- US broadcast deliverables at 29.97: usually drop frame, because program lengths and schedules are in real time. Follow the delivery spec you were given.
- Film, 24, 25 or 50 fps projects: non-drop frame; there is nothing to drop.
- 23.976 projects: non-drop frame. There is no standard drop frame mode at this rate, so conversions must use the exact 24000/1001 rate.
- Internal editing at 29.97: either works, as long as everyone on the project uses the same mode. Mixing modes between editor, caption vendor and delivery is where problems start.
How timecode affects subtitles and captions
Subtitle formats like SRT and WebVTT don't use frames at all. They use real time in hours, minutes, seconds and milliseconds, such as 00:01:00,060 in SRT or 00:01:00.060 in WebVTT (see our SRT vs VTT guide). Any time you move between a frame-based timeline and a subtitle file, you are converting labels to real time, and you have to know which kind of label you started with.
The classic failure: a caption list exported with 29.97 NDF timecode is converted to SRT as though each timecode second were a real second. The first caption is fine. Every later caption is slightly early compared to the video, and the error grows by about 3.6 seconds per hour. On a long lecture or webinar, viewers notice well before the end.
Converting timecode to SRT time in Python
The safe approach is always: timecode to frame count, then frame count to seconds using the exact rate.
from fractions import Fraction
def timecode_to_frames(tc, nominal_fps=30, drop_frame=False):
"""Convert 'HH:MM:SS:FF' or 'HH:MM:SS;FF' to a frame count."""
hh, mm, ss, ff = [int(p) for p in tc.replace(";", ":").split(":")]
frames = ((hh * 3600) + (mm * 60) + ss) * nominal_fps + ff
if drop_frame:
# 2 numbers dropped per minute at 30 fps, 4 at 60 fps
dropped_per_min = 2 if nominal_fps == 30 else 4
total_minutes = hh * 60 + mm
frames -= dropped_per_min * (total_minutes - total_minutes // 10)
return frames
def frames_to_srt(frames, nominal_fps=30, pulldown=True):
"""Frame count to an SRT timestamp. pulldown=True means a 1000/1001 rate."""
rate = Fraction(nominal_fps * 1000, 1001) if pulldown else Fraction(nominal_fps)
total_ms = round(Fraction(frames) / rate * 1000)
h, rem = divmod(total_ms, 3_600_000)
m, rem = divmod(rem, 60_000)
s, ms = divmod(rem, 1000)
return f"{h:02}:{m:02}:{s:02},{ms:03}"
# One hour of 29.97 DF timecode is (almost exactly) one real hour
print(frames_to_srt(timecode_to_frames("01:00:00;00", drop_frame=True)))
# 00:59:59,996
# The same label read as NDF is about 3.6 seconds longer
print(frames_to_srt(timecode_to_frames("01:00:00:00")))
# 01:00:03,600
# 23.976 fps material (NDF, 24 frame labels per second)
print(frames_to_srt(timecode_to_frames("00:10:00:00", nominal_fps=24), nominal_fps=24))
# 00:10:00,600
Two details worth noting. First, Fraction avoids floating point rounding building up over long files. Second, many programs start timecode at 01:00:00:00 rather than zero, so subtract the program's start timecode before converting, or every subtitle will be an hour late.
Common sync bugs and how to spot them
| Symptom | Likely cause | Fix |
|---|---|---|
| Captions in sync at start, drift later | DF or NDF timecode converted as if 30 fps were exactly 30 | Convert with the 1000/1001 rate |
| Drift of about 3.6 s per hour | NDF treated as real time at 29.97 | Use the exact rate when converting |
| Drift of about 0.1 percent on 23.976 content | Rate treated as 24 instead of 24000/1001 | Use 24000/1001 |
| Every caption off by exactly one hour | Program starts at 01:00:00:00 | Subtract the start timecode |
| Invalid timecodes like ;00 at minute 1 | NDF values labeled as DF | Check the source mode |
| Captions off by a fixed amount | Different start points or an intro edited in | Shift all cues by a constant offset |
| PAL and NTSC versions disagree | 25 fps vs 23.976 or 29.97 conversion with speed change | Re-time per version, don't reuse files blindly |
A quick diagnostic: compare the error at the first and last cue. If the error is constant, you have an offset problem. If it grows steadily, you have a rate problem.
Common myths
- "Drop frame drops frames." It drops numbers. Every frame of picture is still there.
- "Drop frame is more accurate." Both count every frame correctly. Drop frame only makes the labels match the wall clock.
- "You need drop frame for 23.976." There is no standard drop frame mode at that rate; use NDF and convert with the exact rate.
- "Web video doesn't have this problem." Plenty of phone and web video is 29.97 or 23.976. It shows up the moment you convert between frames and milliseconds.
Getting caption files that already use real time
If you only need subtitles for a video that lives on YouTube, you can sidestep frame math entirely. YouTubeTranscript.dev exports a video's captions, or AI-generated ones when none exist, as SRT or VTT with millisecond timestamps in real time, so there is no timecode mode to get wrong. If you need to shift or clean those files afterward, the SRT to text and remove timestamps tools help, and our video accessibility guide covers what synchronized captions need to meet WCAG.
Frequently asked questions
What is the difference between drop frame and non-drop frame timecode?+
Both count every frame of 29.97 fps video. Non-drop frame numbers frames continuously at 30 per second, so its labels slowly fall behind real time. Drop frame skips certain frame numbers (not actual frames) so the timecode label stays aligned with the wall clock.
Does drop frame timecode actually drop frames?+
No. No picture is removed. Drop frame only skips frame numbers: labels ;00 and ;01 at the start of every minute, except every tenth minute. The video itself is unchanged.
How do you tell drop frame from non-drop frame timecode?+
By convention, drop frame uses a semicolon before the frame field, as in 01:00:00;00, and non-drop frame uses colons throughout, as in 01:00:00:00. Some software uses a period or other marker, so check the project or file settings when in doubt.
How much does non-drop frame timecode drift?+
At 29.97 fps, non-drop frame timecode falls behind real time by about 3.6 seconds per hour, which is 108 frames.
Is there drop frame timecode for 23.976 fps?+
Not in standard SMPTE timecode. 23.976 fps material is normally counted with non-drop frame timecode, so it drifts against real time in the same way, and conversions must account for the 1000/1001 factor.
Why is NTSC video 29.97 frames per second instead of 30?+
When color was added to the US broadcast standard in the early 1950s, engineers lowered the frame rate by a factor of 1000/1001 to reduce interference between the new color signal and the sound carrier while keeping compatibility with black-and-white TVs. 30 fps became about 29.97 fps.