Frigate Live View Laggy or Frozen? Diagnose It in Three Commands

Frigate Live View Laggy or Frozen? Diagnose It in Three Commands
Photo by Tasha Kostyuk on Unsplash

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:

TechnologySmoothnessRequirements
WebRTCNative frame rate, lowest latencygo2rtc + webrtc.candidates + port 8555 reachable (TCP/UDP)
MSENative frame ratego2rtc + a codec your browser supports
jsmpeg5 to 10 fps, 720p, no audioNone, 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.mp4

A 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" }'
done

Three 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 container ffprobe is not on PATH. 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 mpeg1video

Read 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_source

Each 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:8555

Be 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:

SourceUsable frames over ~10 s
cam_indoor (raw)7
ffmpeg:cam_indoor#video=copy152, 15.1 fps
ffmpeg:cam_indoor#video=h264#hardware=vaapi160, 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_indoor

Two 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.