Skip to content

Fix WAN speed test upload accuracy and GC crash#276

Merged
tvancott42 merged 9 commits intomainfrom
fix/upload-conn-reuse
Feb 15, 2026
Merged

Fix WAN speed test upload accuracy and GC crash#276
tvancott42 merged 9 commits intomainfrom
fix/upload-conn-reuse

Conversation

@tvancott42
Copy link
Collaborator

@tvancott42 tvancott42 commented Feb 15, 2026

Summary

  • Increase concurrent streams from 6 to 8 - Testing showed 8 streams consistently produces better upload throughput (avg 1075 Mbps vs 946 Mbps with 6 streams), while download stays the same (~940 Mbps). Applied to both the Go gateway binary and .NET service.

  • Drain upload response body for TCP connection reuse - HTTP/1.1 requires the response body to be fully read before the connection can be reused. Without this, each upload request opens a new TCP connection instead of reusing the existing one. Fixed in both Go (io.Copy(io.Discard, resp.Body)) and .NET (CopyToAsync(Stream.Null)).

  • Pause iperf3 server during WAN speed tests - The speed test's memory pressure can trigger GC compaction, which corrupts active pipe handles from the iperf3 child process, causing AccessViolationException crashes. The iperf3 server is now paused before the test and resumed after, eliminating the pipe handles during the memory-intensive phase.

  • Remove manual GC calls - The aggressive GC.Collect calls between download/upload phases were the direct cause of the crash. With iperf3 paused, the runtime's own GC can safely compact without corrupting pipe handles.

Test plan

  • 5 consecutive Mac speed tests with 512MB heap cap - no crashes, iperf3 resumed each time
  • 2 gateway speed tests with 8 streams - consistent ~945/1090 Mbps
  • 3 app restarts - iperf3 starts clean each time (orphan cleanup handles port conflicts)
  • Full memory profile without heap cap - runs fine at ~1.7GB RSS, no crash
  • NAS Docker deployment verification

Without reading the response body to EOF before closing, Go's HTTP/1.1
client cannot reuse the connection. Every upload POST was creating a new
TCP + TLS handshake (~8ms overhead per 5MB chunk on 4ms RTT).
Testing showed 8 streams saturates GPON upstream better than 6
(avg 1012 vs 946 Mbps upload). Drain upload response body in .NET
to enable TCP connection reuse. Update UI text to say "multiple"
instead of hardcoding the stream count.
Aggressive GC with compacting corrupts pinned pipe handles from the
iperf3 process stream reader, causing AccessViolationException crashes.
Use Optimized + non-blocking instead to hint the runtime without
forcing memory compaction over active async handles.
Compacting GC moves live objects to new addresses, corrupting in-flight
Spans and pipe handles held by background threads (iperf3 stream reader).
Forced + blocking + no compaction sweeps dead objects without relocating
live ones, safely reclaiming memory while preserving pointer validity.
CompactOnce targets only the Large Object Heap (upload payloads, HTTP
buffers >85KB) without touching the Small Object Heap where pipe handles
and async state machines live. This reclaims the ~1.2 GB upload memory
while avoiding the AccessViolationException from full-heap compaction.
Manual GC.Collect with compacting caused AccessViolationException by
corrupting in-flight Spans and pipe handles. Rather than finding the
right GC incantation, remove all manual GC calls and let the runtime
handle memory pressure naturally. Testing with DOTNET_GCHeapHardLimit
on Mac to validate behavior under memory constraints.
GC compaction while iperf3 pipe readers are active causes
AccessViolationException. Pause the iperf3 process before the
Cloudflare speed test and resume it after.
@tvancott42 tvancott42 merged commit e911dbd into main Feb 15, 2026
1 check passed
@tvancott42 tvancott42 deleted the fix/upload-conn-reuse branch February 15, 2026 00:11
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