[GH-ISSUE #6021] Relay protocol selection (WS vs QUIC) + per-pair throughput ceiling (~25 Mbps with 76% Go runtime overhead) #11462

Open
opened 2026-08-05 01:29:46 -04:00 by saavagebueno · 4 comments
Owner

Originally created by @dlf-dds on GitHub (Apr 28, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/6021

We've been doing a load-test campaign on a self-hosted netbird deployment (v0.70.0 daemon + v0.70.0 relay, official netbirdio/relay:latest image, t3.micro relay VPS in eu-central-1 / il-central-1, two peer-pairs behind separate AWS NAT Gateways forcing relay routing) and surfaced two related observations that we'd like to raise upstream. Both are reproducible.

Original prompt for the investigation: hyper-derp's announcement post (https://hyper-derp.dev/blog/hyper-derp-announcement/), which benchmarks Netbird's relay code as part of the author's preamble. Our findings below partially confirm and partially refine the claims in that post against our own deployment.

1. Daemon picks WebSocket exclusively even when QUIC is available

The relay binary listens for both WebSocket (TCP) and QUIC (UDP) on the same port (:33073):

relay/server/listener/quic/listener.go:39: QUIC server listening on address: :33073
relay/server/listener/ws/listener.go:44:   WS server listening address: :33073

The netbird daemon binary contains the quic-go library (verified via strings | grep quic-go). Yet across our entire deployment (38 active peer connections during the observation window) 100% of relay connections are WebSocket; 0% are QUIC. Every entry in the relay log is WS client connected from <ip>, none are QUIC client connected from <ip>.

Questions for the maintainers:

  • Why does the daemon prefer WebSocket over QUIC when both are available?
  • Is there a config field, env var, or CLI flag to express a protocol preference? netbird up --help shows none; we found QUIC_GO_DISABLE_GSO and QUIC_GO_DISABLE_ECN (quic-go library tunables), but nothing controlling daemon-side protocol selection.
  • Would maintainers consider exposing a relay-protocol-selection interface (env var, config field, or CLI flag) so operators can choose their preferred transport? A prefer_quic toggle would let deployments take advantage of QUIC's congestion-control and HOL-blocking-resistance properties where the network supports UDP.

We'd be happy to send a PR if there's appetite — please point us at the protocol-selection codepath if so.

2. Per-pair throughput caps at ~25 Mbps with 76% of CPU time in Go runtime overhead

iperf3-over-WireGuard between two peers forced into the relay path consistently caps at ~25-30 Mbps single-flow, regardless of:

  • Peer hardware (also tested with bigger peer instances; same ceiling)
  • Linux TCP receive/send buffer sizes (tcp_rmem/tcp_wmem tuned to 16 MB on peers — only ~10% improvement)
  • Docker / docker-proxy presence (extracted relay binary, ran natively on host with --network=host equivalent — single-flow throughput identical to containerized)
  • Kernel rmem_max (strace -c shows ZERO setsockopt calls — relay relies on Linux TCP autotuning, doesn't cap itself)
  • Multi-flow concurrency (4-stream collapses to ~22 Mbps total, 16-stream collapses to ~10 Mbps total — the inverse of expected behavior)

strace -c -f -p <relay-pid> for a 20-second window during sustained load shows:

Syscall Calls Time (s) % of time
futex 16,697 13.53 51.82%
nanosleep 51,581 6.33 24.26%
write 140,320 3.65 13.96%
read 88,565 1.59 6.08%
epoll_pwait 42,860 0.99 3.78%

76% of the relay process's measured CPU time is in futex + nanosleep — Go runtime mutex contention and scheduler park/unpark. Only ~24% is actual network I/O syscalls. Relay process CPU peaked at 33% on the 2-vCPU instance — not CPU-bound at the processor level. This empirically aligns with the "fighting the Go runtime" framing in the hyper-derp post linked above.

We confirmed inter-thread futex contention is part of the picture by sweeping GOMAXPROCS:

GOMAXPROCS Single-flow Mbps
1 36.6
2 (default on 2 vCPU) 27-30
4 34.7

Setting GOMAXPROCS=1 on a 2-vCPU relay yields ~25% improvement — collapses to a single OS thread, eliminates inter-thread futex traffic.

Also worth flagging: when the relay starts with QUIC enabled, quic-go logs:

failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB)

The quic-go library tries to setsockopt the UDP receive buffer to 7 MB but is capped by Linux's default net.core.rmem_max=212992 (208 KB). For deployments that DO use QUIC, this is likely a meaningful per-flow cap — could be worth either a deployment-doc note or adjusting the relay container's expectations.

Questions for the maintainers:

  • Is the per-pair ~25 Mbps ceiling expected? It's roughly 13× lower than direct TCP between the same hosts (which sustains 330 Mbps single-flow / 1 Gbps 4-parallel).
  • Has the GOMAXPROCS=1 improvement been observed by others, and is it documented anywhere we missed?
  • Would the project consider documenting expected per-pair relay throughput as a function of relay instance shape, so operators can plan capacity for sustained-bandwidth workloads (peer-to-peer video, file transfer through relay, etc.)?
  • Is there interest in upstream contributions toward batched-syscall I/O (recvmmsg/sendmmsg) or kTLS offload, which our profiling suggests would help the 24% I/O slice?

Reproducer

The above is summarized from a multi-day RCA campaign — happy to share specific reproducer commands, full strace output, full iperf3 ramps, or run additional measurements if any of this is helpful. Please reply on this issue and we'll provide whatever level of detail is useful.

Thanks for the project!

Originally created by @dlf-dds on GitHub (Apr 28, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/6021 We've been doing a load-test campaign on a self-hosted netbird deployment (v0.70.0 daemon + v0.70.0 relay, official `netbirdio/relay:latest` image, t3.micro relay VPS in eu-central-1 / il-central-1, two peer-pairs behind separate AWS NAT Gateways forcing relay routing) and surfaced two related observations that we'd like to raise upstream. Both are reproducible. Original prompt for the investigation: hyper-derp's announcement post (https://hyper-derp.dev/blog/hyper-derp-announcement/), which benchmarks Netbird's relay code as part of the author's preamble. Our findings below partially confirm and partially refine the claims in that post against our own deployment. ## 1. Daemon picks WebSocket exclusively even when QUIC is available The relay binary listens for **both** WebSocket (TCP) and QUIC (UDP) on the same port (`:33073`): ``` relay/server/listener/quic/listener.go:39: QUIC server listening on address: :33073 relay/server/listener/ws/listener.go:44: WS server listening address: :33073 ``` The netbird daemon binary contains the `quic-go` library (verified via `strings | grep quic-go`). Yet across our entire deployment (38 active peer connections during the observation window) **100% of relay connections are WebSocket; 0% are QUIC**. Every entry in the relay log is `WS client connected from <ip>`, none are `QUIC client connected from <ip>`. **Questions for the maintainers:** - Why does the daemon prefer WebSocket over QUIC when both are available? - Is there a config field, env var, or CLI flag to express a protocol preference? `netbird up --help` shows none; we found `QUIC_GO_DISABLE_GSO` and `QUIC_GO_DISABLE_ECN` (quic-go library tunables), but nothing controlling daemon-side protocol selection. - Would maintainers consider exposing a relay-protocol-selection interface (env var, config field, or CLI flag) so operators can choose their preferred transport? A `prefer_quic` toggle would let deployments take advantage of QUIC's congestion-control and HOL-blocking-resistance properties where the network supports UDP. We'd be happy to send a PR if there's appetite — please point us at the protocol-selection codepath if so. ## 2. Per-pair throughput caps at ~25 Mbps with 76% of CPU time in Go runtime overhead iperf3-over-WireGuard between two peers forced into the relay path consistently caps at ~25-30 Mbps single-flow, regardless of: - Peer hardware (also tested with bigger peer instances; same ceiling) - Linux TCP receive/send buffer sizes (`tcp_rmem`/`tcp_wmem` tuned to 16 MB on peers — only ~10% improvement) - Docker / docker-proxy presence (extracted relay binary, ran natively on host with `--network=host` equivalent — single-flow throughput **identical** to containerized) - Kernel `rmem_max` (`strace -c` shows ZERO `setsockopt` calls — relay relies on Linux TCP autotuning, doesn't cap itself) - Multi-flow concurrency (4-stream collapses to ~22 Mbps total, 16-stream collapses to ~10 Mbps total — the inverse of expected behavior) `strace -c -f -p <relay-pid>` for a 20-second window during sustained load shows: | Syscall | Calls | Time (s) | % of time | |---|---|---|---| | **futex** | 16,697 | 13.53 | **51.82%** | | **nanosleep** | 51,581 | 6.33 | **24.26%** | | write | 140,320 | 3.65 | 13.96% | | read | 88,565 | 1.59 | 6.08% | | epoll_pwait | 42,860 | 0.99 | 3.78% | **76% of the relay process's measured CPU time is in `futex` + `nanosleep`** — Go runtime mutex contention and scheduler park/unpark. Only ~24% is actual network I/O syscalls. Relay process CPU peaked at 33% on the 2-vCPU instance — not CPU-bound at the processor level. This empirically aligns with the "fighting the Go runtime" framing in the hyper-derp post linked above. We confirmed inter-thread futex contention is part of the picture by sweeping `GOMAXPROCS`: | `GOMAXPROCS` | Single-flow Mbps | |---|---| | 1 | **36.6** | | 2 (default on 2 vCPU) | 27-30 | | 4 | 34.7 | **Setting `GOMAXPROCS=1` on a 2-vCPU relay yields ~25% improvement** — collapses to a single OS thread, eliminates inter-thread futex traffic. Also worth flagging: when the relay starts with QUIC enabled, quic-go logs: ``` failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB) ``` The quic-go library tries to setsockopt the UDP receive buffer to 7 MB but is capped by Linux's default `net.core.rmem_max=212992` (208 KB). For deployments that DO use QUIC, this is likely a meaningful per-flow cap — could be worth either a deployment-doc note or adjusting the relay container's expectations. **Questions for the maintainers:** - Is the per-pair ~25 Mbps ceiling expected? It's roughly 13× lower than direct TCP between the same hosts (which sustains 330 Mbps single-flow / 1 Gbps 4-parallel). - Has the `GOMAXPROCS=1` improvement been observed by others, and is it documented anywhere we missed? - Would the project consider documenting expected per-pair relay throughput as a function of relay instance shape, so operators can plan capacity for sustained-bandwidth workloads (peer-to-peer video, file transfer through relay, etc.)? - Is there interest in upstream contributions toward batched-syscall I/O (`recvmmsg`/`sendmmsg`) or `kTLS` offload, which our profiling suggests would help the 24% I/O slice? ## Reproducer The above is summarized from a multi-day RCA campaign — happy to share specific reproducer commands, full strace output, full iperf3 ramps, or run additional measurements if any of this is helpful. Please reply on this issue and we'll provide whatever level of detail is useful. Thanks for the project!
Author
Owner

@jfrconley commented on GitHub (May 28, 2026):

Not affiliated with netbird, but you should never do benchmarking of any kind on a burstable instance. t3.micro is going to burn through the burst credits very quickly and then throttle like mad. You should be using a non-burstable compute tuned instance for this.

You probably got better performance on single thread because you burned through the burst limit slower. Until these results can be validated on a non-burstable instance, they should be considered invalid. This isn't to say that the netbird relay is particularly performant, just that these results as presented are not useful in proving that.

The linked article is interesting, with what appears to be good data. The LLM sounding prose does concern me though...

<!-- gh-comment-id:4567616218 --> @jfrconley commented on GitHub (May 28, 2026): Not affiliated with netbird, but you should never do benchmarking of any kind on a burstable instance. t3.micro is going to burn through the burst credits very quickly and then throttle like mad. You should be using a non-burstable compute tuned instance for this. You probably got better performance on single thread because you burned through the burst limit slower. Until these results can be validated on a non-burstable instance, they should be considered invalid. This isn't to say that the netbird relay is particularly performant, just that these results as presented are not useful in proving that. The linked article is interesting, with what appears to be good data. The LLM sounding prose does concern me though...
Author
Owner

@jfrconley commented on GitHub (May 29, 2026):

Upon follow up, un-scientific testing of my own CPU usage I can confirm two things:

  1. Your benchmarks are likely impacted by burstable instance use
  2. CPU usage of the relay is still astoundingly high

Additionally, QUIC seems to be working fine for my testing. I already had net.core.rmem_max set appropriately.

Testing from hetzner dedicated AMD instance in hetzner with 3 vcpu, I consistently get 200-230 mbps and cpu usage of the relay container is at 200% (I run an unconsolidated container deployment). When setting GOMAXPROCS=1 I get roughly the same bandwidth but with only 100% cpu usage.

I see a couple of issues with this. For one, cpu usage (effective cost) to bandwidth is simply not economical. Cloudflare realtime turn would be an order of magnitude cheaper assuming you rely heavily on relay traffic. Likely faster as well. I had already suspected this was the case and only adopted the relay once external WebRTC was marked as "legacy". I have some doubts about the wisdom of the custom relay approach.

Second, even when this gets fixed there must be a mechanism for metering this usage to relay clients. Allowing a single client to blow out your entire relay budget doesn't make any sense. A default of 40mbps per peer seems reasonable to me.

<!-- gh-comment-id:4579729146 --> @jfrconley commented on GitHub (May 29, 2026): Upon follow up, un-scientific testing of my own CPU usage I can confirm two things: 1. Your benchmarks are likely impacted by burstable instance use 2. CPU usage of the relay is still astoundingly high Additionally, QUIC seems to be working fine for my testing. I already had net.core.rmem_max set appropriately. Testing from hetzner dedicated AMD instance in hetzner with 3 vcpu, I consistently get 200-230 mbps and cpu usage of the relay container is at 200% (I run an unconsolidated container deployment). When setting GOMAXPROCS=1 I get roughly the same bandwidth but with only 100% cpu usage. I see a couple of issues with this. For one, cpu usage (effective cost) to bandwidth is simply not economical. Cloudflare realtime turn would be an order of magnitude cheaper assuming you rely heavily on relay traffic. Likely faster as well. I had already suspected this was the case and only adopted the relay once external WebRTC was marked as "legacy". I have some doubts about the wisdom of the custom relay approach. Second, even when this gets fixed there must be a mechanism for metering this usage to relay clients. Allowing a single client to blow out your entire relay budget doesn't make any sense. A default of 40mbps per peer seems reasonable to me.
Author
Owner

@dlf-dds commented on GitHub (May 31, 2026):

Thanks for digging in, and especially for the dedicated-box numbers — that's the most useful part of this.

On burstable: fair, and I'll concede it for the single-flow figure specifically. But I don't think the
burst-credit mechanism explains the GOMAXPROCS result, and two things cut against it:

  • In my t3.micro runs the relay CPU never went above ~45% (avg ~28%), and a 30s iperf can't drain a fresh
    t3.micro's CPU credits. More importantly, throughput dropped as CPU dropped — down to ~14% at 16 streams.
    Credit throttling does the opposite: it pins CPU at baseline while you try to push harder. What I saw is
    the TCP-over-TCP head-of-line-blocking signature — WireGuard's UDP-shaped retries fighting the outer
    TLS-over-TCP retransmit, buffers ballooning (12 MB → 520 MB in two minutes), throughput collapsing.
  • Your own result is the cleanest refutation of the credit theory: you got the GOMAXPROCS=1 effect (same
    bandwidth, half the CPU) on a non-burstable dedicated AMD box. If burst credits were the cause it wouldn't
    show up there. It matches an strace I ran showing ~76% of relay time in the Go scheduler (futex +
    nanosleep) — inter-thread contention that GOMAXPROCS=1 removes by collapsing to a single OS thread.

And your 200–230 Mbps single-flow actually answers an open question I had. My 31.8 Mbps matched the t3.nano
peer network baseline exactly, so I couldn't tell whether I was measuring the relay or the peer NIC. Your
number says it was the peer NIC — the relay clears 200+ on adequate hardware. So the headline isn't "the
relay caps at 32 Mbps," it's "the relay burns a lot of CPU per Mbps and falls over under multi-flow." Which
is your point, and I agree with it.

So I'd push back on "results should be considered invalid": the CPU-isn't-the-bottleneck-at-multi-flow
finding, the multi-flow collapse, and the GOMAXPROCS win are all hardware-independent, and you confirmed
two of them on bare metal yourself. The one absolute number worth re-running on non-burstable peers is the
single-flow ceiling — and you've basically handed me that already.

GOMAXPROCS=1 is a free ~25% on a 2-vCPU relay until the data plane gets reworked, so worth setting
regardless.

Agree on per-peer metering — one peer saturating a shared relay is a real problem, and a sensible default
cap would help.

On Cloudflare Realtime TURN: agree it's cheaper if you can offload relaying to a third party. Some
deployments can't (trust/sovereignty constraints), so a self-hosted relay still has a reason to exist — it
just needs to stop being this expensive per Mbps.

<!-- gh-comment-id:4588566761 --> @dlf-dds commented on GitHub (May 31, 2026): Thanks for digging in, and especially for the dedicated-box numbers — that's the most useful part of this. On burstable: fair, and I'll concede it for the single-flow figure specifically. But I don't think the burst-credit mechanism explains the GOMAXPROCS result, and two things cut against it: - In my t3.micro runs the relay CPU never went above ~45% (avg ~28%), and a 30s iperf can't drain a fresh t3.micro's CPU credits. More importantly, throughput dropped as CPU dropped — down to ~14% at 16 streams. Credit throttling does the opposite: it pins CPU at baseline while you try to push harder. What I saw is the TCP-over-TCP head-of-line-blocking signature — WireGuard's UDP-shaped retries fighting the outer TLS-over-TCP retransmit, buffers ballooning (12 MB → 520 MB in two minutes), throughput collapsing. - Your own result is the cleanest refutation of the credit theory: you got the GOMAXPROCS=1 effect (same bandwidth, half the CPU) on a non-burstable dedicated AMD box. If burst credits were the cause it wouldn't show up there. It matches an strace I ran showing ~76% of relay time in the Go scheduler (futex + nanosleep) — inter-thread contention that GOMAXPROCS=1 removes by collapsing to a single OS thread. And your 200–230 Mbps single-flow actually answers an open question I had. My 31.8 Mbps matched the t3.nano peer network baseline exactly, so I couldn't tell whether I was measuring the relay or the peer NIC. Your number says it was the peer NIC — the relay clears 200+ on adequate hardware. So the headline isn't "the relay caps at 32 Mbps," it's "the relay burns a lot of CPU per Mbps and falls over under multi-flow." Which is your point, and I agree with it. So I'd push back on "results should be considered invalid": the CPU-isn't-the-bottleneck-at-multi-flow finding, the multi-flow collapse, and the GOMAXPROCS win are all hardware-independent, and you confirmed two of them on bare metal yourself. The one absolute number worth re-running on non-burstable peers is the single-flow ceiling — and you've basically handed me that already. GOMAXPROCS=1 is a free ~25% on a 2-vCPU relay until the data plane gets reworked, so worth setting regardless. Agree on per-peer metering — one peer saturating a shared relay is a real problem, and a sensible default cap would help. On Cloudflare Realtime TURN: agree it's cheaper if you can offload relaying to a third party. Some deployments can't (trust/sovereignty constraints), so a self-hosted relay still has a reason to exist — it just needs to stop being this expensive per Mbps.
Author
Owner

@Silex commented on GitHub (Jul 15, 2026):

Chiming in with data from an independent setup — we hit the same per-pair ceiling, and I think we found the main root cause, with a relatively simple fix.

Setup: two Linux peers (1 Gbit NICs, ~5 ms RTT between them through the relay), stock netbird 0.74.2, a dedicated relay VPS running the stock netbirdio/relay image, relay forced. Baseline: ~950 Mbit/s P2P between the same two peers, but only ~95–100 Mbit/s single-flow through the relay.

Root cause: nothing in the relayed data path ever sizes its UDP socket buffers — your strace observation ("zero setsockopt calls") is exactly it. Every socket runs at the OS default (net.core.rmem_default, typically ~208 KiB). The counterintuitive part: the drops that matter don't happen on the relay at all — they happen in the sending peer's own kernel, on the local UDP socket netbird uses to pull packets out of WireGuard. A single fast flow overruns that ~208 KiB buffer, the kernel drops, the tunnelled TCP sees unexplained loss, and its congestion window collapses to ~95 Mbit/s. We confirmed this by watching per-run netstat -su deltas on all three hosts: the UDP drop counters move on the sender — not on the relay, not on the receiver.

Fix + numbers (single iperf3 flow, cubic, receiver-side Mbit/s, completely stock sysctls):

variant up down
stock 0.74.2 97.6 96.5
+ in-code socket buffer sizing (client-only) 180 150

Retransmissions on the tunnelled TCP go from hundreds per 30 s run to zero. The change sizes the relay-path sockets from inside netbird (SO_RCVBUFFORCE/SO_SNDBUFFORCE when privileged — which the daemon typically is — with portable SetReadBuffer/SetWriteBuffer as fallback), so peers need no sysctl tuning.

PR to fix this issue and gain roughly 1.5-2x perfs is https://github.com/netbirdio/netbird/pull/6774

Other things we tried along the way, some of them also with huge gains (see other PRs)

  • Batch reads + frame coalescing (recvmmsg on the WG socket, packing several packets per relay frame on the WS transport): 180 → 288 Mbit/s on top of the buffer fix. Real gain, but it needs a relay protocol extension, so we split it out. Both parts are now up as follow-ups: #6775 covers the wire-compatible batching + queueing (the 180 → 227 part), and #6776 is the frame-coalescing protocol extension (227 → 288), opened as a draft to discuss capability negotiation first.
  • Striping one flow across N relay connections: no measurable gain — the serial per-packet stages at both ends of the pipeline stay serial, so extra lanes don't help a single flow.
  • BBR on the inner TCP: rides right up to the drop knee, which was useful as a diagnostic (it showed loss, not bandwidth, was the binding constraint), but it's not a fix.
  • On your question #1: recent versions have NB_RELAY_TRANSPORT=ws|quic (shared/relay/client/transport.go) to pick the transport explicitly. Interesting twist: once the buffers are fixed, WS consistently outperformed QUIC in our runs (227 vs 183 Mbit/s with batched reads) — QUIC datagrams are capped by path MTU so they can't carry coalesced frames, and quic-go's receive datagram queue drops silently under bursts.
<!-- gh-comment-id:4978107546 --> @Silex commented on GitHub (Jul 15, 2026): Chiming in with data from an independent setup — we hit the same per-pair ceiling, and I think we found the main root cause, with a relatively simple fix. **Setup:** two Linux peers (1 Gbit NICs, ~5 ms RTT between them through the relay), stock netbird 0.74.2, a dedicated relay VPS running the stock `netbirdio/relay` image, relay forced. Baseline: ~950 Mbit/s P2P between the same two peers, but only ~95–100 Mbit/s single-flow through the relay. **Root cause:** nothing in the relayed data path ever sizes its UDP socket buffers — your strace observation ("zero setsockopt calls") is exactly it. Every socket runs at the OS default (`net.core.rmem_default`, typically ~208 KiB). The counterintuitive part: the drops that matter don't happen on the relay at all — they happen in the **sending peer's own kernel**, on the local UDP socket netbird uses to pull packets out of WireGuard. A single fast flow overruns that ~208 KiB buffer, the kernel drops, the tunnelled TCP sees unexplained loss, and its congestion window collapses to ~95 Mbit/s. We confirmed this by watching per-run `netstat -su` deltas on all three hosts: the UDP drop counters move on the sender — not on the relay, not on the receiver. **Fix + numbers** (single iperf3 flow, cubic, receiver-side Mbit/s, completely stock sysctls): | variant | up | down | |---|---|---| | stock 0.74.2 | 97.6 | 96.5 | | + in-code socket buffer sizing (client-only) | 180 | 150 | Retransmissions on the tunnelled TCP go from hundreds per 30 s run to zero. The change sizes the relay-path sockets from inside netbird (`SO_RCVBUFFORCE`/`SO_SNDBUFFORCE` when privileged — which the daemon typically is — with portable `SetReadBuffer`/`SetWriteBuffer` as fallback), so peers need no sysctl tuning. PR to fix this issue and gain roughly 1.5-2x perfs is https://github.com/netbirdio/netbird/pull/6774 **Other things we tried along the way, some of them also with huge gains (see other PRs)** - *Batch reads + frame coalescing* (recvmmsg on the WG socket, packing several packets per relay frame on the WS transport): 180 → 288 Mbit/s on top of the buffer fix. Real gain, but it needs a relay protocol extension, so we split it out. Both parts are now up as follow-ups: #6775 covers the wire-compatible batching + queueing (the 180 → 227 part), and #6776 is the frame-coalescing protocol extension (227 → 288), opened as a draft to discuss capability negotiation first. - *Striping one flow across N relay connections*: no measurable gain — the serial per-packet stages at both ends of the pipeline stay serial, so extra lanes don't help a single flow. - *BBR on the inner TCP*: rides right up to the drop knee, which was useful as a diagnostic (it showed loss, not bandwidth, was the binding constraint), but it's not a fix. - On your question #1: recent versions have `NB_RELAY_TRANSPORT=ws|quic` (`shared/relay/client/transport.go`) to pick the transport explicitly. Interesting twist: once the buffers are fixed, WS consistently *out*performed QUIC in our runs (227 vs 183 Mbit/s with batched reads) — QUIC datagrams are capped by path MTU so they can't carry coalesced frames, and quic-go's receive datagram queue drops silently under bursts.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#11462