How to Optimize Hysteria Core for High-Latency Networks: BBR Tuning and QUIC Window Sizing

To optimize Hysteria 2 for high-latency networks, enable the BBR aggressive congestion profile and significantly increase QUIC receive window sizes to prevent pipeline starvation on long RTT links.

Hysteria 2, maintained in the apernet/hysteria repository, implements a custom QUIC stack with pluggable congestion control designed to maximize throughput on challenging network paths. When round-trip times (RTT) exceed 200ms, default settings can become overly conservative, causing the congestion window to stall and leaving available bandwidth unused. This guide explains how to optimize Hysteria core for high-latency networks by tuning specific configuration knobs exposed in core/client/config.go and core/server/config.go.

Understanding the Bandwidth-Delay Product Bottleneck

High-latency networks suffer from the bandwidth-delay product (BDP) problem: the amount of data that must be in transit to fully utilize the link capacity increases linearly with RTT. When Hysteria’s default congestion control or QUIC flow control windows are too small for the BDP, the sender idles while waiting for acknowledgments, creating throughput collapse. The Hysteria core addresses this through three tunable areas: congestion control profiles, QUIC-level flow control windows, and connection lifetime timers.

Enabling BBR Aggressive Congestion Control

The most impactful optimization involves selecting the BBR congestion control algorithm with the aggressive profile. BBR estimates bottleneck bandwidth and RTT to pace packets efficiently, but the aggressive profile specifically raises the pacing gain and congestion-window gain constants to maintain higher flight sizes when feedback is delayed.

Configure this via the CongestionConfig struct:

// core/client/config.go (mirrored in core/server/config.go)
type CongestionConfig struct {
    Type       string `mapstructure:"type"`        // "bbr" or "reno"
    BBRProfile string `mapstructure:"bbrProfile"`  // "standard" or "aggressive"
}

During initialization, the verifyAndFill function (client) or fill function (server) normalizes these values:

c.CongestionConfig.Type, err = congestion.NormalizeType(c.CongestionConfig.Type)
if c.CongestionConfig.Type == congestion.TypeBBR {
    c.CongestionConfig.BBRProfile, err = congestion.NormalizeBBRProfile(c.CongestionConfig.BBRProfile)
}

The profile constants are defined in core/internal/congestion/bbr/bbr_sender.go. The aggressive profile sets a higher congestionWindowGainConstant and more permissive pacing gain, allowing the sender to inject more packets per RTT before backing off. This directly compensates for the longer feedback loop in high-latency environments.

Sizing QUIC Receive Windows for Long RTT

QUIC flow control prevents stream stalls by advertising receive windows. On high-latency links, small windows cause the sender to block before the receiver has processed buffered data. Increase these values in the QUICConfig struct to match your BDP:

Field Purpose Recommended High-Latency Value
InitialStreamReceiveWindow Per-stream buffer at connection start 16-32 MiB
MaxStreamReceiveWindow Maximum per-stream buffer growth 16-32 MiB
InitialConnectionReceiveWindow Aggregate connection buffer at start 80-100 MiB
MaxConnectionReceiveWindow Maximum aggregate buffer 80-100 MiB

In core/client/config.go, these fields map directly to the underlying quic-go transport parameters. The defaults (8 MiB stream, 20 MiB connection) are sufficient for low-latency LANs but inadequate for satellite links.

Connection Stability and Timer Tuning

High-latency networks often exhibit jitter that can trigger premature connection teardown. Extend these timers in QUICConfig:

  • MaxIdleTimeout: Prevents connection closure during slow periods. Set to 120s or higher via QUICConfig.MaxIdleTimeout.
  • KeepAlivePeriod: Controls health probe frequency. Set to 30s via QUICConfig.KeepAlivePeriod to reduce overhead on lossy paths.
  • UDPIdleTimeout: Specific to Hysteria’s UDP mode in core/server/config.go. Raise this to 180s to prevent the internal UDP socket from closing during gaps in traffic.

PMTU discovery sends probe packets to determine the maximum transmission unit, but on high-latency or tunneled networks, these probes may be dropped or delayed, causing fallback to smaller 1280-byte packets and degraded throughput. Disable PMTU discovery when operating behind fixed-MTU tunnels:

QUICConfig{
    DisablePathMTUDiscovery: true, // Defined in core/internal/pmtud/avail.go
}

How Configuration Maps to Runtime

When a client or server starts, the chosen congestion algorithm is applied through congestion.UseConfigured in core/internal/congestion/utils.go:

func UseConfigured(conn *quic.Conn, congestionType, bbrProfile string) {
    switch congestionType {
    case TypeReno:
        return // Reno is a no-op placeholder
    default:
        UseBBR(conn, bbr.Profile(bbrProfile))
    }
}

This factory function creates a bbrSender instance (from core/internal/congestion/bbr/bbr_sender.go) with the selected profile constants. The aggressive profile’s higher gain values translate directly into larger in-flight data allowances, which is essential for filling high-RTT pipes.

Practical Configuration Examples

Create a server.yaml that applies all high-latency optimizations:

listen: ":443"
tls:
  cert: /etc/hysteria/server.crt
  key: /etc/hysteria/server.key
congestion:
  type: bbr
  bbrProfile: aggressive
quic:
  initStreamReceiveWindow: 16777216    # 16 MiB

  maxStreamReceiveWindow: 16777216
  initConnReceiveWindow: 83886080      # 80 MiB

  maxConnReceiveWindow: 83886080
  maxIdleTimeout: 120s
  keepAlivePeriod: 30s
  disablePathMTUDiscovery: false       # Set true if behind tunnel

udpIdleTimeout: 180s

Programmatic Client Configuration

When embedding Hysteria as a library, construct the config struct directly:

package main

import (
    "net"
    "time"
    
    "github.com/apernet/hysteria/core/v2/client"
    "github.com/apernet/hysteria/core/v2/internal/congestion"
)

func main() {
    cfg := &client.Config{
        ServerAddr: &net.UDPAddr{IP: net.ParseIP("203.0.113.1"), Port: 443},
        CongestionConfig: client.CongestionConfig{
            Type:       congestion.TypeBBR,
            BBRProfile: "aggressive",
        },
        QUICConfig: client.QUICConfig{
            InitialStreamReceiveWindow:     16 << 20,
            MaxStreamReceiveWindow:         16 << 20,
            InitialConnectionReceiveWindow: 80 << 20,
            MaxConnectionReceiveWindow:     80 << 20,
            MaxIdleTimeout:                 2 * time.Minute,
            KeepAlivePeriod:                30 * time.Second,
            DisablePathMTUDiscovery:        false,
        },
    }
    
    cli, err := client.NewClient(cfg)
    if err != nil {
        panic(err)
    }
    _ = cli // use for opening streams
}

Summary

  • Enable BBR aggressive profile via congestion.type: bbr and congestion.bbrProfile: aggressive in your YAML, or CongestionConfig{Type: congestion.TypeBBR, BBRProfile: "aggressive"} programmatically, to increase pacing gain and congestion window growth constants defined in bbr_sender.go.
  • Increase QUIC receive windows by setting initStreamReceiveWindow and maxStreamReceiveWindow to 16-32 MiB, and initConnReceiveWindow to 80+ MiB, allowing the bandwidth-delay product to fill high-RTT links without blocking.
  • Extend connection timers by raising maxIdleTimeout to 120s and keepAlivePeriod to 30s to prevent teardown during jittery periods, and set udpIdleTimeout to 180s for UDP relay stability.
  • Disable PMTU discovery using disablePathMTUDiscovery: true when operating behind tunnels that do not properly handle ICMP or packetization probes.

Frequently Asked Questions

What is the difference between BBR standard and aggressive profiles in Hysteria?

The standard BBR profile uses conservative pacing and congestion window gains suitable for general internet conditions, while the aggressive profile—defined in core/internal/congestion/bbr/bbr_sender.go—increases the congestionWindowGainConstant and pacing rate. This allows the sender to maintain higher in-flight data volumes specifically to overcome the bandwidth-delay product limitations of high-latency networks.

Calculate the BDP by multiplying your bandwidth by the RTT. For a 100 Mbps link with 500ms RTT, you need approximately 6.25 MB of buffer. Set initStreamReceiveWindow and maxStreamReceiveWindow to at least 8 MiB (8388608 bytes) and initConnReceiveWindow to 40-80 MiB to account for multiple concurrent streams.

Can I use the brutal congestion controller instead of BBR for high-latency networks?

Yes. The brutal controller located in core/internal/congestion/brutal/brutal.go implements a simple bandwidth-flooding algorithm that ignores RTT feedback and sends at a fixed configured rate. While this avoids BBR's RTT-probing overhead, it requires accurate manual bandwidth configuration and lacks BBR's fairness mechanisms. Use brutal only if you have a dedicated, uncontended link with known fixed capacity.

Why would I disable Path MTU Discovery on a high-latency network?

PMTU discovery relies on probing for the maximum packet size supported by the path. On high-latency networks, especially those traversing tunnels or satellite links, these probes may be dropped or delayed, causing the QUIC stack to falsely assume a lower MTU and fragment traffic. Disabling PMTU discovery via disablePathMTUDiscovery: true forces the use of a safe default (often 1280 bytes) and prevents throughput degradation from spurious MTU reduction, provided your network infrastructure can handle the smaller packets efficiently.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →