Performance & Latency Tuning
System-level optimizations for Linux kernels, GPU governors, CPU pinning, and network transport.
Ready-made profiles#
Copy one profile into ~/.config/sunshine/sunshine.conf, then tune Moonlight bitrate to the link. Measure with the client overlay before changing more keys.
Wired NVIDIA, 1080p120 or 1440p120.
busy_poll_us = 50
rate_cap_pct = 90
enet_4mib_buffer = true
dscp_qos = true
cpu_pinning = true
gpu_governor = true
latency_mode = aggressive
pipewire_latency_ms = 4
nvenc_tuning_preset = 0CPU Frequency Governor & Pinning#
For 120 FPS + 4K streaming, verify your CPU governor is set to performance and enable SolarFlare CPU pinning:
cpu_pinning = true elevates the capture worker to SCHED_RR real-time policy and pins the thread to a non-IRQ core, bypassing general OS context-switch overhead.
# Check current scaling governor
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Set all cores to performance
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governorGPU Clocks & Power Governors#
- AMD GPUs: Setting
gpu_governor = truein sunshine.conf forcespower_dpm_force_performance_leveltoperformanceduring active streaming. - NVIDIA GPUs: Prevent GPU down-clocking during video capture using clock locking (
nvidia-smi -lgc <boost_clock>).
# Query maximum graphics boost clock
nvidia-smi --query-gpu=clocks.max.graphics --format=csv,noheader
# Lock GPU clock to boost frequency (e.g. 2100 MHz)
sudo nvidia-smi -lgc 2100,2100Network Buffers & Low-Latency Polling#
Wi-Fi and high-bitrate LAN links benefit from expanded socket buffers and busy-polling:
enet_4mib_buffer = trueExpands socket buffers so 50+ Mbps bursts do not get dropped in kernel send queues.
busy_poll_us = 50Enables kernel SO_BUSY_POLL for instant UDP wakeup without burning full CPU cores.
dscp_qos = trueMarks streaming packets with DSCP CS3 so QoS-enabled routers prioritize stream traffic.
Recommended Kernel sysctls#
Add the following tweaks to /etc/sysctl.d/99-streaming.conf:
# Increase socket max buffer sizes for 4K streaming
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 4194304
net.core.wmem_default = 4194304
# Enable BBR congestion control
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbrLatency deep-dive: measure first#
Read the Moonlight overlay (decode + network + render) next to host GET /api/stream/latency (capture_ms, encode_ms, network_total_ms, network_queue_dwell_ms, rtt_ms) and GET /api/stream/telemetry (CPU/GPU/RAM pressure). If capture_ms is high, suspect the compositor or CPU pinning; if encode_ms is high, drop a preset level or resolution; if network/rtt dominates, run iperf3 UDP at the Moonlight bitrate and keep loss under 5% with jitter under 1 ms before touching knobs.
iperf3 -s
iperf3 -c <host> -u -b 80M -t 10
ping -c 100 <host> | tail -3
curl -sk -H "Authorization: Bearer $TOK" https://localhost:47990/api/stream/latency | jq .capture_ms,.encode_ms,.rtt_msLink, buffers & adaptive bitrate#
Link speed is auto-detected from /sys/class/net/<iface>/speed; a 2.5 GbE host feeding a 1 GbE client must cap with rate_cap_pct (70 or lower on Wi-Fi) or buffers overrun. enet_4mib_buffer expands socket buffers for 50+ Mbps bursts. For lossy links enable adaptive_bitrate_enabled with sane min/max bounds, raise fec_percentage, and set Opus sf_opus_fec plus sf_opus_expected_loss_pct. DSCP CS3 helps only if the router and AP honor QoS/WMM. Note the BBR sysctl below shapes TCP control traffic, not the UDP stream itself.
adaptive_bitrate_enabledLet client network feedback move the bitrate between min and max at runtime.
adaptive_bitrate_enabled = truefec_percentageForward error correction redundancy for the video stream. Raise on lossy Wi-Fi.
fec_percentage = 30packetsizeRTP packet size. 0 leaves MTU discovery automatic; lower only on fragmented paths.
packetsize = 1392Vendor & link profiles#
Start from the closest profile, then A/B one knob at a time and re-measure:
encoder = vaapi
capture = kms
latency_mode = safe
pipewire_latency_ms = 8HDR streaming chain#
HDR needs the whole chain: KMS capture, an HDR compositor (Plasma 6, Gamescope), a 10-bit encoder (hevc_mode or av1_mode 3+), HDR enabled in host OS and Moonlight client, and an EDID emulator or HDR display on the host. Washed-out output means something fell back to SDR — check the colorspace lines in the log and the IsHdrSupported negotiation.
Symptom to knob table#
| Symptom | Check | Knob |
|---|---|---|
| KMS black + Probably not permitted | getcap, /dev/dri perms | setcap; video group |
| vainfo fails | libva driver present | Install driver or encoder = software |
| No monitor | xrandr --listmonitors | VIRTUAL1 or headless_mode |
| No sink | pactl/wpctl sinks | audio_sink = name |
| Loss over 5% | iperf3 UDP | rate_cap_pct down; adaptive on |
| 401 from API | Credentials | sunshine --creds reset |
| 403 from API | Scope / origin / CSRF | Token scope; origin_web_ui_allowed; X-CSRF-Token |
| Update stuck waiting_idle | GET /api/sessions | End stream or cancel |