Why one TCP connection can be slow on a fast line

Tools that open many parallel connections, such as iperf3 -P or multi-connection download managers, often achieve much higher speed than a single transfer over the same line. This usually happens for one of two reasons:

  • Per-flow traffic shaping: the provider or a middlebox limits bandwidth per TCP flow (source and destination address and port) rather than only capping the line as a whole.
  • High latency and packet loss: on long-distance paths with a high bandwidth-delay product, loss-based TCP congestion control shrinks its sending window after a dropped packet and takes many round trips to recover.

Opening more application streams inside one connection does not avoid either limit, because all traffic still shares the same outer TCP flow.

Method TCP flows App interface Per-flow limit
Single TCP connection 1 1 stream limited to one flow
Multiplexed streams (HTTP/2, yamux) 1 many streams limited to one flow
Parallel transfers (iperf3 -P) many many connections needs app support
Aggligator many 1 stream combines flows

Splitting one stream across multiple flows

Aggligator accepts a single byte stream from the application and sends its packets across multiple independent transport links. The receiving endpoint reorders the incoming packets back into one stream and resends unacknowledged data if a link fails.

To get higher throughput in practice, three settings matter:

  • Multiple flows at the bottleneck: open several parallel links with set_links_per_path on the TCP, SOCKS5 or WebSocket connector (or --tcp-links and --socks-links in agg-tunnel).
  • Buffer sizes: on high-latency paths with many links, increasing send_buffer and recv_buffer in the configuration allows enough in-flight data to keep all links busy.
  • Congestion control: enabling BBR (--tcp-congestion bbr) prevents occasional packet loss from throttling individual TCP links.

In a practical user report on a line where each outgoing TCP flow was throttled to 2.5 Mbit/s, combining 16 parallel flows with Aggligator raised the throughput of a single logical connection to about 180 Mbit/s.

Direct TCP: multiple parallel links per path

When connecting directly over TCP, calling set_links_per_path on TcpConnector (or passing --tcp-links to agg-tunnel) opens multiple TCP connections with distinct source ports over each network path.

Parallel TCP links over one path A client and a server each hold one byte stream. Between them run four separate TCP connections over the same network path, each subject to a per-flow limit, combining their bandwidth into one stream. Client alc::Stream 1 byte stream Server alc::Stream server.example:5800 Same network path — 4 separate TCP flows TCP link #0 :50338 → :5800 TCP link #1 :50348 → :5800 TCP link #2 :50360 → :5800 TCP link #3 :50366 → :5800
Each link uses its own TCP socket and ephemeral source port, so a per-flow traffic shaper treats them as separate TCP flows while the application reads and writes one stream.
Four parallel TCP links in Rust
use aggligator_transport_tcp::TcpConnector;

let mut tcp = TcpConnector::new(["server.example"], 5800).await?;
tcp.set_links_per_path(4);

let mut connector = Connector::new();
connector.add(tcp);

let stream = connector.channel().unwrap().await?.into_stream();
Four parallel TCP links with agg-tunnel
agg-tunnel client --tcp server.example:5800 --tcp-links 4 --port 22:2222

SOCKS5 proxies: multiple tunnel or proxy paths

When traffic goes through local tunnel instances (such as ssh -D or an encrypted proxy client) or remote SOCKS5 servers, the SOCKS5 transport can combine several proxy endpoints, or open multiple parallel links through one proxy with set_links_per_path (--socks-links).

Aggregating links through SOCKS5 proxies A client distributes one stream across four SOCKS5 proxy endpoints. Each proxy establishes its own independent outer TCP connection to the server, where the packets are reassembled into one stream. Client alc::Stream 1 byte stream Server alc::Stream server.example:5800 SOCKS5 proxies Independent outer TCP flows 127.0.0.1:1081 Outer flow #1 127.0.0.1:1082 Outer flow #2 127.0.0.1:1083 Outer flow #3 127.0.0.1:1084 Outer flow #4
Each local tunnel or SOCKS5 proxy establishes its own independent outer TCP connection to the server, while Aggligator distributes the stream across all of them.
Combining multiple SOCKS5 proxy paths in Rust
use aggligator_transport_socks::{Socks5Connector, Socks5Target};

let target = Socks5Target::parse("server.example", 5800)?;
let proxies = ["127.0.0.1:1081", "127.0.0.1:1082", "127.0.0.1:1083", "127.0.0.1:1084"];

let mut connector = Connector::new();
connector.add(Socks5Connector::new(proxies, target)?);

let stream = connector.channel().unwrap().await?.into_stream();
Forwarding a port over multiple SOCKS5 proxies with agg-tunnel
agg-tunnel client \
    --socks 127.0.0.1:1081 --socks 127.0.0.1:1082 \
    --socks 127.0.0.1:1083 --socks 127.0.0.1:1084 \
    --socks-target server.example:5800 \
    --port 22:2222

Questions

Why does multiplexing streams inside one connection not help?

Stream multiplexers such as HTTP/2, yamux or SSH connection sharing still send all data through one outer TCP connection. A traffic shaper that limits each flow by its address and port pair applies the same limit regardless of how many streams are multiplexed inside it. Aggligator instead distributes one stream across several separate transport connections.

Does this increase speed when the whole line is already saturated?

No. Combining multiple flows only helps when individual TCP connections are held below the capacity of the line, for example by per-flow traffic shaping or single-connection window limits on high-latency paths. It cannot exceed the total bandwidth of the internet connection.

How many parallel links can be opened over one interface?

By default, the TCP, SOCKS5 and WebSocket transports establish one link per path. Calling set_links_per_path on their connector (or passing --tcp-links or --socks-links to agg-tunnel) opens multiple parallel connections over each path.

Is this suitable for real-time traffic such as voice or gaming?

Aggligator provides an ordered, reliable byte stream. When a packet is lost on one link, later packets received on other links wait in the receive buffer until the missing packet is retransmitted. This head-of-line blocking does not hurt bulk transfers or shell sessions, but can add latency spikes to real-time UDP traffic tunneled over TCP.

Do both ends need to run Aggligator?

Yes. Packets sent over the separate flows must be reordered and reassembled at the receiving end, so the server must run Aggligator or agg-tunnel as well.