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

> Optimize Hysteria core for high-latency networks. Tune BBR congestion control and increase QUIC window sizes to boost performance on long RTT links.

- Repository: [Aperture Internet Laboratory/hysteria](https://github.com/apernet/hysteria)
- Tags: performance
- Published: 2026-05-13

---

**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`](https://github.com/apernet/hysteria/blob/main/core/client/config.go) and [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/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:

```go
// 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:

```go
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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/core/server/config.go). Raise this to 180s to prevent the internal UDP socket from closing during gaps in traffic.

## Path MTU Discovery on Lossy Links

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:

```go
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`](https://github.com/apernet/hysteria/blob/main/core/internal/congestion/utils.go):

```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`](https://github.com/apernet/hysteria/blob/main/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

### YAML Configuration for Satellite Links

Create a [`server.yaml`](https://github.com/apernet/hysteria/blob/main/server.yaml) that applies all high-latency optimizations:

```yaml
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:

```go
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`](https://github.com/apernet/hysteria/blob/main/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`](https://github.com/apernet/hysteria/blob/main/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.

### How large should I set the QUIC receive windows for a 500ms RTT link?

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`](https://github.com/apernet/hysteria/blob/main/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.