For a few months one camera on our server had a habit. Every so often the live view would turn choppy, the timeline would stutter when you scrubbed through it, and the whole thing felt like the camera had dropped to five frames a second. Then, hours later, it would be fine again.
Every dashboard said nothing was wrong. Detection was running at a clean 5 fps. The stats page was green. CPU was normal. The camera’s own web page showed a smooth 25 fps stream. We spent hours over several weeks suspecting the camera, the network, the hardware decoder, and the phone.
It was none of them. The camera was recording perfectly. It was just recording into the wrong number of files.
What was actually happening
Frigate records continuously by running one ffmpeg process per camera that writes the stream to disk in ten-second pieces. Six files a minute, three hundred and sixty an hour, per camera. Playback stitches those pieces back together, and at ten seconds each the seams are invisible.
What we found on the bad days was that the affected camera was writing a new file at every keyframe instead of every ten seconds. On most of our cameras a keyframe arrives every two seconds, so that meant thirty files a minute. On one camera with a one-second keyframe interval it was sixty. The video inside each file was fine. There was just five to ten times too many of them, and the player was stitching hundreds of one-second clips together and showing you every seam.
That is the “choppy” you see. Not dropped frames. Seams.
Why nothing warned us
Frigate does have a recording watchdog, and it is a good one. It restarts the recording process for a camera when there have been no new valid segments for two minutes. That catches the common failure, which is a camera going silent.
It cannot catch this one, because during a fragmentation episode segments are arriving constantly. Tiny ones, but valid ones. From the watchdog’s point of view the camera has never been healthier.
And the episode never ends on its own. We went back through the recording folders and found roughly thirteen of these since May, and the only thing that had ever cleared one was the recording process getting restarted for some unrelated reason. One camera once spent an hour writing 1,801 files before a chance restart rescued it.
We also confirmed the trigger sits in the timestamps rather than in the camera or the network. Starting a fresh ffmpeg with the exact same command Frigate uses, against the same stream, produced perfect ten-second files immediately, while the long-running process next to it was still cutting at every keyframe. Something upstream sends a timestamp discontinuity, ffmpeg’s segment muxer loses track of where the current segment started, and from then on it cuts at the next keyframe every time. We have not pinned down the upstream cause yet. That part is still open.
The check you can run in ten seconds
Frigate stores recordings as recordings/YYYY-MM-DD/HH/<camera>/MM.SS.mp4, and the hour folder is in UTC, not local time. So the only tool you need is a file count.
To see the last complete hour for every camera on a typical Docker install:
d=$(date -u -d "1 hour ago" +%Y-%m-%d)
h=$(date -u -d "1 hour ago" +%H)
for c in /path/to/frigate/storage/recordings/$d/$h/*/; do
printf "%-20s %s\n" "$(basename $c)" "$(ls -1 $c | wc -l)"
done
Reading the number:
| Files in one hour | What it means |
|---|---|
| About 360 | Healthy. Ten-second segments. |
| 400 or more | Fragmenting. Look at the per-minute count below. |
| 1,800 or 3,600 | Cutting at every keyframe (2 s or 1 s GOP). Playback for that hour will stutter. |
| Far below 360 | A different problem. The camera stalled or went offline, and Frigate’s own watchdog is the right tool for that. |
If an hour looks high, the per-minute breakdown tells you exactly when it started:
ls -1 /path/to/recordings/2026-09-11/13/garage/*.mp4 \
| xargs -n1 basename | cut -d. -f1 | sort | uniq -c
Six per minute is healthy. The moment it jumps to twenty-eight and stays there, you have found your episode.
The fix, once you know a camera is stuck, is just to restart its recording process. Killing that camera’s record ffmpeg inside the container is enough. Frigate notices within about thirty seconds, starts a fresh one, and the fresh one writes ten-second files again. No container restart, no config change, no interruption to the other cameras.
Turning the check into a watchdog
Once we understood the failure, the manual fix was easy but the noticing was not. It happens at random hours, it is silent, and the first sign is usually someone opening the app and finding the timeline stuttering. So we wrote a small script and put it on a two-minute cron.
It is about eighty lines of bash and it does exactly one thing: count segments per camera for the last complete minute, and if a camera has been over the threshold for two consecutive checks, kill its recording process and let Frigate respawn it. A few design choices turned out to matter more than the counting:
- Look at the last complete minute, never the current one. The current minute is half-written and will always look low.
- Require two strikes. One high minute can be a burst around a restart. Two in a row two minutes apart is the real thing.
- Zero is not a trigger. A camera writing nothing is a different failure with a different owner. Frigate’s own watchdog handles it, and the last thing you want is two watchdogs restarting the same process for different reasons.
- A per-camera cooldown, and a cap per pass. We started with a one-hour cooldown and a limit of three cameras per run. Within a week a storm hit five cameras at once, the watchdog fixed three, and then a second wave re-broke two of them minutes later, inside the cooldown. They would have sat broken for another thirty-five minutes. The cooldown is fifteen minutes now.
- A heartbeat file. The log is empty when everything is healthy, which is what you want, but it also means an empty log looks identical to a dead cron. The script touches a file every run. If that file is ever more than two minutes old, the watchdog itself is the problem.
- Log first, act second. There is a one-line switch that turns it into alert-only. We ran it that way first, with the threshold lowered on purpose to make sure the strike and cap logic fired the way we expected, before letting it kill anything.
How it has gone
The watchdog has been running since late August. In that time it has restarted a camera’s recording process 60 times across 14 different days. The worst minute it caught was 60 files, a camera cutting a new segment every single second. It has logged zero errors. A stuck camera is now caught within about four minutes, or fifteen at the outside when it lands inside a cooldown, compared to the hours or days it used to sit.
The episodes have not stopped. Some days it is quiet, some days it fires three times. Which is the honest part of this post: the script treats the symptom. The root trigger is still upstream somewhere in the restream, and we will write that up when we actually find it rather than when we have a theory.
But the timeline scrubs clean now, and that is what anyone opening the app actually cares about.
If your playback stutters
Before you blame the camera, the phone, or the network, count the files. It takes ten seconds, it needs no tools you do not already have, and in our experience it has been the answer more often than everything else combined.
If the count is high, restart that camera’s recording process and watch it drop back to six a minute. If it keeps coming back, a two-minute cron with a file count and a kill is a smaller fix than it sounds, and you can run it in log-only mode for a week before you let it touch anything.
Earlier in this series: Pull the cable covers what still works when your internet goes out, and The power side covers keeping the recorder alive through an outage.