Skip to content

Fix nginx buffering the webcam stream into RAM on the fluidd interface - #38

Open
arlophoenix wants to merge 2 commits into
Guilouz:mainfrom
arlophoenix:fix/nginx-webcam-buffering-tmpfs-guilouz
Open

Fix nginx buffering the webcam stream into RAM on the fluidd interface#38
arlophoenix wants to merge 2 commits into
Guilouz:mainfrom
arlophoenix:fix/nginx-webcam-buffering-tmpfs-guilouz

Conversation

@arlophoenix

Copy link
Copy Markdown

The fluidd server block in files/moonraker/nginx.conf proxies /webcam*/ without proxy_buffering off, while the mainsail block in the same file has it on all four locations. Because nginx's compiled proxy_temp_path is /var/tmp/nginx/proxy and /var/tmp is a symlink to /tmp, which is tmpfs, a client that drains the MJPEG stream slower than the camera produces it makes nginx spool the difference into RAM. The default proxy_max_temp_file_size is 1024 MB against a ~100 MB tmpfs on a ~200 MB machine, so nothing bounds it.

On a K1C 2025 this exhausted memory in about two minutes mid-print. Shmem went from a flat 11.8 MB baseline to 93.5 MB in ninety seconds and plateaued at the tmpfs cap; the kernel evicted the entire buffer cache, moonraker logged a 7.17 second blocked event loop, klipper lost communication with nozzle_mcu, power-loss recovery auto-resumed the print, and the OOM killer took moonraker. That MCU's retransmit counter sat at exactly 9 for the preceding two hours and has been 0 since, so the link was fine — the host was starved. The OOM dump attributes 93,796 kB to shmem that no process mapped, which is unmapped tmpfs file data. The same signature recurred on a second date peaking at 91,400 kB.

This is not confined to port 4408. scripts/creality_web_interface.sh inserts listen 80; after listen 4408 default_server; when the user selects fluidd as the port-80 interface, so on those installs the unprotected block is what answers a bare http://<printer>/webcam/.

The first commit gives the four fluidd /webcam*/ locations the same five directives the mainsail block already carries — it only makes the file consistent with itself, and the mainsail block is untouched. The second sets proxy_max_temp_file_size 0 in the http block as a backstop, so nginx passes proxied responses through synchronously and never writes a temp file; a slow client then throttles the upstream instead of consuming RAM. The two are separable if you want only the first. Pointing proxy_temp_path at /usr/data would also stop the RAM growth, but trades it for continuous eMMC writes on every stream, so it is deliberately not the fix here.

Verified on a K1C 2025 (mid-print) and a K1C OG (standby): staged nginx -t before and after install, graceful nginx -s reload with the master PID unchanged both times so klipper and moonraker were never interrupted, then fluidd 200 on 4408 (and on port 80 for the OG), moonraker API 200, a ~71 KB snapshot and 5.3 MB of stream in 5 s.

Test plan

  • grep -c "proxy_buffering off" files/moonraker/nginx.conf returns 8; diff is 21 insertions and 0 deletions, so the mainsail block is provably unchanged
  • nginx -t passes on the rendered config, before and after install
  • Fresh install → fluidd loads, webcam stream plays, moonraker API responds
  • Slow-client test (50 KB/s against a ~1.06 MB/s source, a 21:1 mismatch, for 15 s): /tmp usage and Shmem stayed flat, swap untouched, zero files created under /tmp/nginx/proxy

Not addressed

The same unbounded-buffering exposure applies to any large proxied response, notably /server/files/... gcode downloads that routinely run ~100 MB; the proxy_max_temp_file_size 0 backstop covers those generically rather than being webcam-specific. Separately, moonraker has no restart supervision on these boxes, so an OOM kill from any cause leaves the web UI down until reboot — out of scope here.

…nsail

The mainsail server block sets postpone_output 0, proxy_buffering off and
proxy_ignore_headers X-Accel-Buffering on every /webcam*/ location; the fluidd
block sets none of them. With buffering left on, nginx spools an MJPEG stream a
client cannot drain fast enough into its proxy temp directory. That directory is
the compiled default /var/tmp/nginx/proxy, and on CrealityOS /var/tmp is a
symlink to /tmp, which is tmpfs -- so the spill lands in RAM.

This only makes the file consistent with itself; the mainsail block is unchanged.
proxy_max_temp_file_size defaults to 1024 MB. On these boxes the proxy temp path
resolves into a ~100 MB tmpfs on a ~200 MB machine, so the default is an
invitation to exhaust RAM. Setting it to 0 makes nginx pass proxied responses
through synchronously and never write a temp file, so a slow client throttles
the upstream instead of consuming memory.

This covers every proxied response, not just the webcam -- including large
/server/files/... gcode reads, which are routinely ~100 MB.

Deliberately not relocating proxy_temp_path to /usr/data: that would trade RAM
exhaustion for continuous eMMC writes on every stream.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant