Streaming leaks memory, and one flag fixes it
Skip this for A, B and C: nothing is encoded in those, so none of it applies. It is only for D and E.
NVIDIA’s video encoder loses a little memory every time it is reconfigured, never giving it back. Unreal reconfigures it whenever the streaming bitrate changes. With ordinary adaptive streaming, this is constant, so the Unreal machine’s committed memory climbs for as long as anyone watches. The issue is tracked upstream and the fault lies in NVIDIA’s driver rather than Unreal (see below). However, one flag works around it today.
This affects a kiosk severely because the viewer is the big screen’s browser, not the visitor standing in front of it. That browser stays connected from opening until something restarts it, running the leak all day regardless of footfall. On our box, running portrait 720×1280 with one viewer:
nobody watching |
flat |
one viewer connected |
over 2 GB per minute |
viewer leaves |
stops at once, and nothing is released |
Two gigabytes a minute is more than a terabyte across a working day.
The fix
Run streamingDemoStreamedKioskWorkaround.bat instead of streamingDemo.bat. It is the same
launcher plus one flag:
-PixelStreamingEncoderTargetBitrate=12000000
Holding the bitrate still means there is nothing to reconfigure. On our box, this reduced the growth from over 2 GB a minute to zero measurable. If you launch Unreal your own way rather than from our scripts, just add the flag; the file exists so you do not have to.
Picking the number — and 12 is very likely right. The pin is not a picture-quality setting. Its only job is to stop the value moving, because movement leaks memory. Where you put it barely matters, provided it sits above what the encoder actually wants. The encoder never pads up to it, so headroom above the real rate costs nothing while absorbing keyframes and fast head turns. What genuinely determines picture sharpness and network payload is the quality ceiling — see Picture quality.
To verify this yourself, open chrome://webrtc-internals on the big screen’s browser and watch the
inbound video stream for half a minute once it settles. At our shipped ceiling
(EncoderMaxQuality=60), expect somewhere near 6 Mbit/s — comfortably under the pinned 12, which is
the point. Do not follow it down. Pinning at what you observe removes headroom, making the pin the
binding constraint instead of the ceiling, which is a blunter way to do the same job.
The one trade. A pinned bitrate cannot drop when the network dips, meaning a bad moment stutters instead of quietly going softer. The question is whether your link is reliably fast, not whether it is technically the internet. A wired link between two fixed sites usually is; anything mobile or contended is not.
If your link is not reliable, lower the ceiling, not the pin. EncoderMaxQuality=55 puts about 4.5
Mbit/s on the wire instead of 5.7, leaving the pin where it is. Do not take the pin below about 8
Mbit/s: once it drops under what the ceiling is asking for, it starts overriding the ceiling and you
are tuning the same thing twice from two directions.
Either way, not pinning is not a safe fallback. A nightly restart does not save you. The arithmetic is not close: over two gigabytes a minute exceeds a terabyte across a working day, and nobody has a terabyte of memory to wait for 4 a.m.
Checking it worked
Two checks. Skipping either wastes the exercise.
Did the flag apply? The log must contain this exact line. A similar line ending from Property
[EncoderTargetBitrate] is the project setting that this one overrides, so look for from command
line.
Setting CVar [PixelStreaming2.Encoder.TargetBitrate] and Property [EncoderTargetBitrate] to [12000000] from command line
Did the growth stop? Leave a viewer connected for twenty minutes and watch Task Manager → Performance → Memory → "Committed". Flat passes. Still climbing steadily fails — tell us, because it changes what we can honestly recommend.
Three signs it is genuinely fixed rather than just slower: committed memory and working set move together again instead of drifting apart, the figure goes down as often as up, and the memory returns immediately when the viewer disconnects. A leak does none of those.
Is a real fix coming?
Not soon, and not from Unreal. The leak is inside NVIDIA’s encoder driver, and NVIDIA has acknowledged it, so the proper fix must arrive in a future driver release. Epic knows about it too, but their engine work has moved on to the next major version, meaning an Unreal-side fix is not coming soon.
Treat the flag as permanent rather than a stopgap. When you next update the NVIDIA driver on the Unreal machine, you can spend twenty minutes re-running the check above without the flag. Until that comes back flat, keep it.
One dead end, in case you read about it upstream: Epic’s other suggested workaround, the launch flag
-AVCodecs.NvEnc.D3D12UsesCUDA=true, is not a way out. We tested it here; the video froze within two
minutes and the stream refused subsequent reconnections. Use the bitrate flag above.
Keep the nightly restart anyway — but it is not the fix
Schedule one for a time when the kiosk is closed; both supervisors under the machine that runs Unreal do it on a timer. It costs nothing and clears out whatever else drifts across a day.
It does not solve this problem, and cannot. A restart is the only thing that returns the leaked memory, but it only happens once a night, and the leak consumes over a terabyte in the sixteen hours between. You cannot buy your way out of that with RAM. You also cannot simply restart more often: in a streamed kiosk, the viewer is the big screen’s browser, making a restart during opening hours a blank wall and a visible reconnect for anyone standing there.
Pinning is the fix. The restart is housekeeping.
If you ever suspect it again
Judge it by Committed and nothing else. RAM usage, working set and VRAM all stay flat while this happens, which is exactly why it goes unnoticed.
Do not wait for the machine to run out of memory. Windows quietly grows the pagefile to stay ahead — we watched one box drag its own limit from 77 GB past 185 GB — so it survives far longer than the arithmetic suggests, eventually running out of disk instead. "It streamed for half an hour and was fine" proves nothing. When it finally runs out, Unreal usually reports "Out of video memory" or the process simply vanishes leaving no crash dump. Neither points directly to memory; both mean this leak.
When the engine fix ships, drop the pin and keep the restart.