Skip to content
11 min read

We turned SFU transcoding off — the transcode line went to zero and two things broke

The saving is real and it is one flag. What nobody writes down is the second job the transcoder was doing, and the third. Here is what breaks, how each failure presents, and the two-command audit that tells you in advance whether your sources survive the trade.

Vyacheslav Pankratov· Fullstack Developer
Cover art for “We turned SFU transcoding off — the transcode line went to zero and two things broke”
On this page

TL;DR: Disable SFU transcoding and the transcode-minutes line goes to zero — on WHIP ingress it is one flag, and a transcode-less import does not consume connection minutes either. Two things stop working in exchange, and neither announces itself: the server can no longer hand a weak connection a smaller layer, and it stops normalising the camera's H.264 into something every browser will accept. A two-command pre-flight audit tells you which of your sources survive the trade, before you make it.

There is a moment on every managed real-time platform where somebody opens the invoice, sorts the lines by size, and finds that the largest one is transcoding. It is a rational thing to want gone. The technical work is trivial — on WHIP ingress it is a single boolean — and the saving is immediate and total, not incremental.

This article is about what the invoice does not tell you: the transcoder was doing three jobs, you were being billed for one of them, and the other two only become visible after they stop. Neither failure shows up in staging, and one of them is invisible to every dashboard you own.

What is the transcode line actually paying for?

Per-minute re-encoding of media that is already encoded. On LiveKit's published rates, transcode minutes on the Scale tier run $0.015 per minute of video after 8,000 included, and $0.004 for audio-only; the Ship tier is $0.02 (livekit.com/pricing, re-read 2026-08-01 — rates and inclusions move, so check rather than quote this).

The number that matters is not the rate, it is the multiplier. Transcoding is billed per publish-minute, not per view-minute. A meeting product publishes while people are in meetings. A camera fleet publishes twenty-four hours a day, and every one of those minutes is billed whether a human is watching or not. That single asymmetry is why the same feature is a rounding error for one product and the dominant line for another.

The multiplication is one line, and it is the only one you need in order to decide whether this article applies to you at all. An always-on source publishes 60 × 24 × 30 = 43,200 minutes a month; at the Scale tier's $0.015 that is $648 a month, per camera, before anybody watches a single frame. The same rate against a source publishing eight hours a day is $216, and against one publishing an hour a day, $27. Nothing about the source changed between those three numbers — only how long it was switched on.

Two honest caveats on that arithmetic. Account-level included minutes (8,000 on Scale, shared with recording and export) take the edge off the first source and nothing off the tenth. And this is deliberately the unit, not a fleet model: multiply it by your own count, because what a large fleet costs — and what the remedies for it cost, which is the less obvious number — is its own modelling exercise rather than a paragraph here.

Before deciding anything, check whether the line is even the size it should be: publishers created on registration rather than on first view, and idle publishers that nothing shuts down, inflate it without changing the product. Those are configuration rather than architecture, they are frequently worth more than this whole decision, and they belong to the forensic piece on the bill itself. Fix them first — the trade below is worth making on a number that is already honest.

Turning transcoding off removes the line rather than reducing it, and there is a second saving worth knowing about: LiveKit's ingress documentation notes that transcode-less imports are free and do not consume connection minutes either. On WHIP the flag is enable_transcoding, and per livekit/protocol "Only WHIP supports disabling transcoding currently. WHIP will default to transcoding disabled." (The older bypass_transcoding is deprecated and means the same thing.)

  • 24 h/day — a camera$648/mo
  • 8 h/day$216/mo
  • 1 h/day — a meeting$27/mo
One source, one published rate, three publishing schedules. The camera is not more expensive to transcode than the meeting — it is only switched on longer.

What breaks first: nobody makes a smaller layer any more

The transcoder was not only re-encoding, it was producing the encodings the server hands to weak subscribers. Take it away and the server has exactly what the camera sent — one stream, at one resolution and one bitrate — and it forwards that to everybody, because an SFU selects among layers and never creates them. That mechanism, and the four fixes that do not work, are their own article.

The presentation is specific enough to recognise: desktop viewers and low-resolution sources stay fine, while high-resolution streams stall for anyone on a weak or mobile connection. We verified this on a real device over a mobile network rather than reasoning about it, which matters because the failure is invisible from an office: the same stream on the same build over wifi is perfect.

This is the half that people anticipate, at least vaguely. It is also the half with cheap remedies — publishing the camera's existing sub-stream, or a publisher that emits real simulcast — so it is rarely the reason a rollback happens.

What breaks second, and silently: nobody normalises the codec

This is the one that costs a day, because nothing about it looks related to a billing change.

With transcoding on, the server re-encodes everything and every camera-side codec quirk is quietly normalised on the way through. With it off, whatever the camera produces reaches the browser untouched — and browsers differ in how strict they are about what they accept. The failure we hit was iOS Safari showing a black rectangle while Android and desktop played the same stream, caused by a level the camera declares in its SPS being above the one iOS negotiates. It is silent, it is platform-specific, and the transport counters all look healthy while it happens. Mechanism, the getStats signature that identifies it, and a fix that costs no CPU are in the article on that failure.

The general form is worth more than the specific bug: a component that silently normalises something, and always succeeds, leaves no trace in any document. Nobody writes down a job that never fails. When you remove such a component for a good reason, the thing to inventory is not what it was billed for — it is what it was quietly guaranteeing.

How do you know which sources survive passthrough?

The good news is that the whole risk collapses to a single question you can answer per source before changing anything: what actually arrives from this camera? With transcoding on it did not matter, which is exactly why nobody knows the answer. Two commands.

What the source declares:

ffprobe -v error -rtsp_transport tcp -select_streams v:0 \
  -show_entries stream=codec_name,profile,level,width,height,pix_fmt,has_b_frames \
  -of default=noprint_wrappers=1 "rtsp://<camera>/<path>"

Whether a second, smaller encoding already exists — run the same command against the camera's sub-stream path, because most IP cameras publish one and it costs nothing to use. Hikvision, for instance, addresses main and sub as separate RTSP channels, /Streaming/channels/101 and /102 (Hikvision support). On a camera we probed on a staging rig, the main stream was 1080p at level 4.0 and the sub-stream 720p at level 3.1 — the second one being both smaller and the only one of the two an iPhone would accept as it stands.

Read the results against this, which is the routing table the audit produces:

What arrives Passthrough verdict
H.264, level ≤ 3.1, limited range, no B-frames safe — this is what you are hoping for
H.264, level above 3.1, or full range (yuvj420p) fixable without re-encoding — rewrite the SPS in flight
H.264 with B-frames or a long GOP test it on a real strict decoder before trusting it
H.265 check per client class — see below, the answer changed recently
MJPEG safe, because it has to be re-encoded anyway

The H.265 row is the one that has moved, so do not inherit an older article's answer, including an older version of this one. Chromium shipped HEVC in WebRTC in milestone 136, and Safari has it too — but the Intent to Ship is explicit that "H265 encoding is only available if the user's device and operating system provide the necessary capabilities as we will not provide a software implementation to fall back to" (blink-dev). So support is real and hardware-dependent at the same time: a viewer whose device lacks the hardware has no software path to fall back on. Test it against the client mix you actually have rather than a compatibility table.

Keeping a source on re-encode is not free either. A software re-encode costs roughly 1 vCPU per camera, which is what makes the exception list a budget rather than a footnote — and why the goal of the audit is to make that list short, not to make it empty.

The point of the table is that most sources land in the first two rows, and the second row costs nothing to fix. That is what makes the flip fast: the audit is cheap, it is per-source, and it turns "will this break?" into a list with names on it.

  1. Probe every sourceone ffprobe per camera
  2. Sort into verdictssafe · fixable · keep transcoding
  3. Rewrite the fixableSPS metadata, no re-encode
  4. Flip the safe onesper source, never globally
  5. Verify on real devicesa phone on mobile data, and an iPhone
The audit is what makes the flip reversible: every source has a verdict before anything changes, and the two silent regressions get an explicit check at the end.

When should you leave transcoding on?

Naming this matters, because an article that concludes "always turn it off" is a sales page wearing a post-mortem's clothes.

Leave it on when the fleet is small. The saving scales with publish-minutes; below a few always-on sources the line is not what is wrong with your bill, and you would be buying an audit and a routing decision for pennies.

Leave it on when your sources are heterogeneous and outside your control. Passthrough turns every camera's codec configuration into your problem, and if new sources arrive continuously from customers you do not administer, that is a permanent tax rather than a one-off audit.

Leave it on when nobody owns the media path. The failures above are silent and platform-shaped. If there is no one to notice that iPhones stopped working, the transcoder is buying you insurance that is cheaper than the incident.

Turn it off when the fleet is large, always-on, and yours — a known set of cameras, a known codec profile, and somebody who will run the audit. That is the case where the line is dominant and the risks are enumerable.

Neither regression will report itself

The invoice will confirm the saving within a day. Nothing will tell you that weak connections stopped adapting, or that one client class stopped decoding — both look healthy from every dashboard you own. Subscribe from a phone on real mobile data and confirm the picture holds; subscribe from an iPhone and confirm frames are decoded, not merely received.

FAQ

Is this a LiveKit-specific decision? The flag is, the trade is not. Any SFU that offers transcoding is offering the same three jobs in one product — re-encode, produce layers, normalise codecs — and any decision to stop paying for the first also stops the second and third. The names differ; the questions to ask before flipping do not.

Will the saving really be the whole line? The transcode line, yes — it is billed per minute of transcoding, so no transcoding is no minutes. Bandwidth and connection minutes are separate and unaffected, except that on LiveKit a transcode-less import also avoids connection minutes. Model it against your own invoice rather than expecting a percentage from an article.

Can we turn it off for some cameras and not others? Yes, and that is usually the right shape. The audit is per source, so its output is a routing decision per source: passthrough for the clean ones, an in-flight SPS rewrite for the ones that only declare a bad level, and re-encoding kept for the handful that need it. Global switches are what make this scary; per-source routing is what makes it boring.

How do we know it worked, beyond the invoice? Check the two regressions explicitly, because neither reports itself: subscribe from a real phone on a real mobile network and confirm the picture holds, and subscribe from an iPhone and confirm frames are decoded rather than merely received. Both take minutes and both fail silently if you skip them.

The three things worth taking away

  1. Transcoding is billed per publish-minute, not per view-minute. That asymmetry, not the rate, is what makes it the dominant line for an always-on fleet and a rounding error for a meeting product.
  2. You are removing three jobs and paying for one. Layer production and codec normalisation stop at the same moment as the billing, and neither announces itself — one degrades only on weak networks, the other only on strict decoders.
  3. The audit is per source and cheap, so do it first. One ffprobe per camera turns an irreversible-feeling decision into a routing table where most rows say "safe" and the rest say "fixable without re-encoding".

If you are looking at that line on your own invoice right now, the honest sequence is: audit the sources, fix the ones that only need a metadata rewrite, keep re-encoding for the exceptions, and flip the rest. In that order — the audit is what makes the flip reversible.


Deciding this for a real fleet — what the audit finds, what it costs to keep the exceptions, and whether the saving survives the remedies — is exactly the scope of an Architecture Sprint: measured, costed, and yours to act on with or without us.

// Build it

Running this in production?

Real-time video breaks in the places a demo never reaches — negotiation, weak networks, the transcoding bill. Tell us what breaks and where; we reply within a day with a concrete next step.