Both clients send a Ping frame roughly every 700-1300 ms for the entire life of the tunnel. The interval starts at one second (ios/APNExtension/APNProvider.swift:288, heartbeat.start(interval: 1); android/.../vpn/Tun.kt:96, hrt.start(1)) and the loop simply reschedules itself at jitter.millis(1s) -> a uniform 0.7-1.3x (ios/APNExtension/ICMPKeepAlive.swift:221; android/.../vpn/ICMPKeepAlive.kt:150). It never adapts to RTT, activity, or idleness. Android compounds this by holding a 24-hour PARTIAL_WAKE_LOCK for the whole session (android/.../vpn/VpnCore.kt:207), keeping the CPU warm about once a second no matter what.
This is wasteful because the node's idle read timeout is 60s (node/internal/server/phone.go:23, refreshed on every frame), so the client only needs to ping about once a minute to stay alive — today it pings roughly 60x more often than required while idle. That is a real battery and data cost (battery is a listed test area), and it is also a stealth liability: theories.md (Theory 2 residual) notes the idle steady state is a jittered but still periodic beacon, which is a classic flow-classifier signal, whereas Xray/Vision emits no heartbeat at all. WireGuard's passive keepalive is 10s and fires only after receiving data with nothing to send, and its persistent-keepalive defaults off (25s when used, purely to hold NAT).
Make the interval adaptive: keep ~1 Hz only while data is actively flowing (plus a short tail), then back off exponentially toward the node's 60s timeout (e.g. 25-45s) when idle, and release or shorten the Android wakelock while idle. The Pong carries lease/fleet state, but that tolerates 30-60s idle freshness, so backing off is safe; keep the existing jitter. Filing per a WireGuard comparison review.
Both clients send a Ping frame roughly every 700-1300 ms for the entire life of the tunnel. The interval starts at one second (
ios/APNExtension/APNProvider.swift:288,heartbeat.start(interval: 1);android/.../vpn/Tun.kt:96,hrt.start(1)) and the loop simply reschedules itself atjitter.millis(1s)-> a uniform 0.7-1.3x (ios/APNExtension/ICMPKeepAlive.swift:221;android/.../vpn/ICMPKeepAlive.kt:150). It never adapts to RTT, activity, or idleness. Android compounds this by holding a 24-hourPARTIAL_WAKE_LOCKfor the whole session (android/.../vpn/VpnCore.kt:207), keeping the CPU warm about once a second no matter what.This is wasteful because the node's idle read timeout is 60s (
node/internal/server/phone.go:23, refreshed on every frame), so the client only needs to ping about once a minute to stay alive — today it pings roughly 60x more often than required while idle. That is a real battery and data cost (battery is a listed test area), and it is also a stealth liability:theories.md(Theory 2 residual) notes the idle steady state is a jittered but still periodic beacon, which is a classic flow-classifier signal, whereas Xray/Vision emits no heartbeat at all. WireGuard's passive keepalive is 10s and fires only after receiving data with nothing to send, and its persistent-keepalive defaults off (25s when used, purely to hold NAT).Make the interval adaptive: keep ~1 Hz only while data is actively flowing (plus a short tail), then back off exponentially toward the node's 60s timeout (e.g. 25-45s) when idle, and release or shorten the Android wakelock while idle. The
Pongcarries lease/fleet state, but that tolerates 30-60s idle freshness, so backing off is safe; keep the existing jitter. Filing per a WireGuard comparison review.