Frigate Live View Laggy or Frozen? Diagnose It in Three Commands
Your recordings are perfect. Detection works. Object clips play back smoothly at full frame rate. But the live view in the Frigate UI looks like a slideshow: a few frames per second, or a picture that simply stops moving while the clock burned into the image keeps ticking in the recordings.
Search for this and you get the same three answers on every forum thread. It's your network. Buy a Coral. Your CPU is too weak. In my case none of those were true. The recordings proved the cameras, the network and the disks were all fine, so whatever was broken lived strictly between go2rtc and the browser.
It turned out to be three unrelated problems producing one identical symptom, and the only way through was to stop guessing and measure what the browser actually receives. That test is the useful part of this article, and it transfers to any browser video-streaming problem, Frigate or not.
Everything below was diagnosed on Frigate 0.17.2 with go2rtc 1.9.10, running in Docker, with a mix of Reolink and Eufy cameras. Camera and stream names in the examples are generic; substitute your own.
First, understand what Frigate is actually playing
Frigate has three live technologies, and it picks the best one that works, silently. That silence is the root of the confusion:
| Technology | Smoothness | Requirements |
|---|---|---|
| WebRTC | Native frame rate, lowest latency | go2rtc + webrtc.candidates + port 8555 reachable (TCP/UDP) |
| MSE | Native frame rate | go2rtc + a codec your browser supports |
| jsmpeg | 5 to 10 fps, 720p, no audio | None, it is the universal fallback |
When WebRTC and MSE both fail, the UI falls back to jsmpeg without telling you. That fallback is exactly what your eye reads as "roughly 1 fps". It isn't a performance problem, it isn't your network, and no amount of CPU will fix it: you are simply being served a deliberately degraded stream.
So the first question is never "why is it slow?" It is "which of the three am I even watching?"
The test that ends the guessing
Here is the mistake almost every thread makes, and I made it too: probing the RTSP source. Of course the source looks healthy. The recorder is using that same source and the recordings are flawless. Probing the input tells you nothing about a problem that happens on the output.
Instead, ask go2rtc for exactly what it hands the browser for MSE, the fragmented MP4, and count the frames in it:
# Grab ~12 seconds of the actual browser-facing stream
curl -s -m 12 "http://127.0.0.1:1984/api/stream.mp4?src=YOUR_CAMERA" -o /tmp/probe.mp4
# List the presentation timestamp of every decodable frame
ffprobe -v error -select_streams v:0 -show_entries frame=pts_time -of csv=p=0 /tmp/probe.mp4A healthy 15 fps camera gives you roughly 160 lines, spaced about 0.066 s apart. Anything dramatically different is your answer. Wrapped up so it prints a verdict per camera:
for cam in "$@"; do
curl -s -m 12 "http://127.0.0.1:1984/api/stream.mp4?src=$cam" -o /tmp/probe.mp4
echo "== $cam ($(stat -c %s /tmp/probe.mp4) bytes)"
ffprobe -v error -select_streams v:0 -show_entries frame=pts_time -of csv=p=0 /tmp/probe.mp4 \
| awk -F, '{ t=$1+0; if (n++) { d=t-p; s+=d; if (d>mx) mx=d } p=t }
END { if (n>1) printf " frames=%d duration=%.1fs fps=%.1f avg_gap=%.3fs max_gap=%.3fs\n", n, s, (n-1)/s, s/(n-1), mx;
else print " no decodable frames" }'
doneThree numbers matter. Frame count tells you whether the stream exists at all. fps tells you whether it is being throttled. And avg_gap versus max_gap tells you whether the timeline is regular, because a stream can carry the right number of frames and still stutter if they are unevenly spaced.
Run it against a camera you know is smooth and one you know is not. The difference points straight at which of the three causes below you are dealing with.
Note on paths. Inside the official Frigate containerffprobeis not onPATH. It lives at/usr/lib/ffmpeg/7.0/bin/ffprobe, with the older build at/usr/lib/ffmpeg/5.0/bin/.
Cause 1: Smart Streaming, working as designed
If the live view shows a picture that refreshes every second or so and only bursts into motion when something happens, nothing is broken. That is Frigate's Smart Streaming, and it is the default on the dashboard grid: still image most of the time, real stream on activity. It exists to save bandwidth and CPU, and for most people it is the right default.
The setting is easy to miss because it does not live where you would expect. It is not in config.yml, and there is no YAML equivalent at all. Go to Live → pick a camera group → the pencil (Edit) icon on the group. The dialog offers, per camera: No streaming, Smart Streaming, Continuous Streaming.
Two consequences that cost me time:
- A camera that belongs to no group cannot be configured. The group edit dialog is the only place this setting exists, so ungrouped cameras are stuck on the default. If a camera is missing from the dialog, add it to a group first.
- The preference is stored in your browser's
localStorage. It is per device and per browser. Set it on your laptop and your phone still uses the default. Clear your site data and it is gone.
Opening a single camera full-screen bypasses the whole thing and gives you the real stream, which is a quick way to confirm this is your cause before touching anything.
Cause 2: the silent jsmpeg fallback
If full-screen is also choppy, you are being served jsmpeg. Server side, the tell is one ffmpeg process per camera transcoding to MPEG-1:
ps aux | grep mpeg1videoRead that carefully, though. Frigate keeps those processes around as a safety net, so their mere existence proves nothing. What matters is whether your browser is consuming one.
Two configuration mistakes force a permanent fallback.
The camera name does not match the go2rtc stream name
This one is quietly brutal. Frigate auto-maps a camera's live view to the go2rtc stream bearing the same name. No match means no MSE and no WebRTC source, so jsmpeg forever, with no error anywhere in the logs.
I hit it with a camera declared as cam_outdoor whose go2rtc streams were named outdoor_source and outdoor_1080_vaapi. Nothing was wrong with either the camera or the streams, they just did not share a name. The fix is to declare the mapping explicitly:
cameras:
cam_outdoor:
live:
streams:
Outdoor 1080p: outdoor_1080_vaapi
Outdoor source: outdoor_sourceEach entry becomes a selectable option in the UI, which is a nice side effect: you get a quality switcher. Verify what Frigate actually resolved:
curl -s localhost:5000/api/config | jq '.cameras | map_values(.live.streams)'WebRTC without ICE candidates
go2rtc can be listening on 8555 in both TCP and UDP and still be unusable. With no ICE candidate advertised, the browser has no address to negotiate with, gives up, and walks down the ladder to MSE and then jsmpeg.
go2rtc:
webrtc:
listen: 0.0.0.0:8555
candidates:
- 192.168.1.50:8555 # the LAN address of your Frigate host
- stun:8555Be realistic about scope here. A private-IP candidate only works on your LAN. From outside, through a reverse proxy, WebRTC will not establish and you will land on MSE, which is still full frame rate, so this is a latency trade-off rather than a quality one. For WebRTC off-LAN you would need to advertise a public or VPN address (a Tailscale 100.x address works well) and actually open 8555.
Bonus trap: hitting the camera directly instead of the restream
While in there, check that no camera reads its RTSP URL directly in ffmpeg.inputs when a go2rtc stream of the same name already exists. If it does, watching the live view opens a second RTSP session against the camera, and consumer cameras often only tolerate one or two. The result is freezing that looks exactly like every other symptom in this article.
inputs:
- path: rtsp://127.0.0.1:8554/cam_indoor
roles: [record, detect]Everything goes through the restream. One producer, and consumers you can count:
curl -s localhost:1984/api/streams | jq 'map_values({p: (.producers|length), c: (.consumers|length)})'Cause 3: the real bug, a stream with no timeline
This is the one that no forum thread had, and the one the frame-counting test cracked open.
My Eufy cameras froze on the first frame while the Reolinks played perfectly, side by side, same server, same config shape. ffprobe on the go2rtc stream showed a completely healthy source: 1080p15, 2-second GOP, 1.5 Mbit/s, no packet loss. By every measure of the input, nothing was wrong.
Then I ran the test above on what the browser receives:
== cam_indoor (740 KB)
frames=7 ...
== cam_outdoor (...)
frames=162 duration=10.6s fps=15.3 ...Seven decodable frames in 740 KB. Not a bandwidth problem, the bytes were all there. The timestamps were the problem: every frame carried a presentation time about one millisecond after the previous one. From the browser's point of view the entire ten seconds of video claims to happen inside a single instant. So it renders the first frame and waits, forever, for a timeline that never advances.
The cause is that these Eufy cameras emit malformed RTP timestamps. In pure copy mode go2rtc faithfully passes them through into the fMP4, producing a track with no usable timeline. Frigate's own ffmpeg, the one doing recording and detection, normalises them without complaint, which is exactly why the recordings were perfect and hid the bug.
The fix: remux, don't re-encode
Routing the stream through ffmpeg with -c copy is enough to rebuild the timestamps. No re-encoding, no quality loss, negligible CPU. Measured on the same camera, same ten seconds:
| Source | Usable frames over ~10 s |
|---|---|
cam_indoor (raw) | 7 |
ffmpeg:cam_indoor#video=copy | 152, 15.1 fps |
ffmpeg:cam_indoor#video=h264#hardware=vaapi | 160, 15.1 fps |
Hardware re-encoding fixes it too, but it burns GPU to solve a metadata problem. The remux is the right answer. In practice, declare a dedicated live stream per affected camera and leave the raw stream to recording and detection:
go2rtc:
streams:
cam_indoor:
- rtsp://... # the camera itself
cam_indoor_live:
- ffmpeg:cam_indoor#video=copy # references the stream above, not the camera
cameras:
cam_indoor:
live:
streams:
Live: cam_indoor_live
Direct source: cam_indoorTwo properties make this cheap. The *_live stream references the existing go2rtc stream, not the camera, so no additional RTSP session is opened, which matters a lot on cameras that only allow one or two. And go2rtc spawns the ffmpeg process on demand, only while a browser is watching, so idle cost is zero.
After the change, all three affected cameras delivered 15.1 fps with a 0.067 s inter-frame gap where maximum gap equalled average gap: a perfectly regular timeline, in fact steadier than the Reolinks playing their native stream.
A word on the "GPU Stats unavailable" warning
While chasing this I burned real time on a red herring worth naming, because it appears in the same UI and looks related.
Frigate showed GPU Stats unavailable, which reads like "hardware acceleration is broken" and sends you straight down the VAAPI rabbit hole. It is a known bug in Intel's intel_gpu_top reporting, not a Frigate bug and not an acceleration failure. On my Intel N-series box, the global i915 engine counters and power.GPU were frozen at zero while frequency.actual and rc6 still varied, per-client counters still worked, and scale_vaapi was visibly running in the ffmpeg command lines. Decoding was fine the whole time. A host reboot clears it, temporarily.
The lesson generalises past Frigate: the first error message a UI shows you is almost never the problem you have. It is just the one with a banner.
The takeaway
Three problems, one symptom, and the diagnostic that separates them fits on one line: measure the output, not the input.
Probing the RTSP source is the intuitive move and it is useless here, because the source is fine in all three cases. The moment I consumed exactly what the browser consumes, /api/stream.mp4, and counted frames and gaps, each cause identified itself immediately. A normal frame count at a low rate means Smart Streaming or jsmpeg. A collapsed frame count with intact bytes means a broken timeline.
That technique is not Frigate-specific. Any time a video plays somewhere else but not in a browser, the question is not "is the source good?" It is "what exactly did the browser receive, and does its timeline make sense?"
Related reading: Record a 4K camera stream using GPU transcoding with Frigate and go2rtc and When cloud cameras go dark: lessons from today's Eufy outage.