# How Hysteria Core Handles Concurrent Connections: QUIC, Goroutines, and Session Management

> Explore how Hysteria core manages concurrent connections using QUIC, goroutines, and session management. Learn about its layered architecture and state protection.

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

---

**Hysteria manages concurrent connections through a layered goroutine architecture that spawns dedicated threads for each QUIC connection and individual TCP streams, while using `sync.RWMutex` and per-session `sync.Mutex` to protect shared UDP session state.**

The `apernet/hysteria` repository implements a high-throughput proxy core built on QUIC (HTTP/3) that scales to thousands of simultaneous flows. Rather than using thread pools or complex schedulers, the codebase embraces Go’s goroutine-per-connection model with strategic synchronization primitives to ensure thread safety without sacrificing performance.

## Connection-Level Concurrency: The QUIC Listener

At the entry point, `serverImpl.Serve` in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) runs an infinite acceptance loop. Each time the QUIC listener accepts a new connection, the server immediately delegates handling to a new goroutine.

This isolation ensures that a single client’s QUIC session operates independently:

```go
func (s *serverImpl) Serve() error {
    for {
        conn, err := s.listener.Accept(context.Background())
        if err != nil { return err }
        go s.handleClient(conn)   // ← new goroutine per client
    }
}

```

The `handleClient` method then initializes an HTTP/3 server instance with a custom `StreamDispatcher`, creating a dedicated execution context for all streams within that connection.

## Stream-Level Parallelism for TCP Traffic

Inside `handleClient`, Hysteria hijacks incoming HTTP/3 streams via `h3sHandler.ProxyStreamHijacker`. Rather than processing streams sequentially, the dispatcher spawns a new goroutine for every TCP request:

```go
func (h *h3sHandler) ProxyStreamHijacker(ft http3.FrameType, stream *quic.Stream, err error) (bool, error) {
    if ft == protocol.FrameTypeTCPRequest && err == nil && h.authenticated {
        qStream := &utils.QStream{Stream: stream}
        go h.handleTCPRequest(qStream) // ← concurrent handling per stream
        return true, nil
    }
    return false, nil
}

```

This design allows dozens of TCP streams to proxy simultaneously over a single QUIC connection without head-of-line blocking. The goroutine-per-stream model matches Hysteria’s goal of maximizing throughput for bursty traffic patterns.

## UDP Session Architecture

UDP traffic follows a different concurrency pattern. Instead of per-packet goroutines, Hysteria uses a centralized `udpSessionManager` instantiated in [`core/server/udp.go`](https://github.com/apernet/hysteria/blob/main/core/server/udp.go).

### The Manager Loop

When UDP is enabled, `handleClient` starts the manager’s `Run` loop in its own goroutine:

```go
func (m *udpSessionManager) Run() error {
    stopCh := make(chan struct{})
    go m.idleCleanupLoop(stopCh) // ← cleanup runs in its own goroutine
    defer close(stopCh)
    for {
        msg, err := m.io.ReceiveMessage()
        if err != nil { return err }
        m.feed(msg) // feed updates/creates per‑session entries
    }
}

```

This loop receives all UDP datagrams for the connection and routes them to appropriate session entries based on `SessionID`.

### Per-Session State and Cleanup

Each UDP session is represented by a `udpSessionEntry` stored in a protected map:

- **Shared state protection**: The map `map[uint32]*udpSessionEntry` is guarded by a `sync.RWMutex`. Lookups during active traffic acquire read locks, while new session creation requires write locks.
- **Per-session isolation**: Each entry owns its own UDP connection and runs `receiveLoop` in a dedicated goroutine.
- **Race prevention**: A per-entry `sync.Mutex` (`connLock`) guarantees that a connection cannot be created after the session has been closed.
- **Resource reclamation**: An `idleCleanupLoop` runs every second to remove stale sessions based on configurable timeouts, preventing memory leaks from abandoned UDP flows.

## Synchronization Strategy

Hysteria’s concurrency model distributes locks across layers to minimize contention:

| Layer | Primitive | Purpose |
|-------|-----------|---------|
| **Connection acceptance** | Goroutine per connection (`handleClient`) | Isolates each client’s QUIC session |
| **TCP streams** | Goroutine per request (`handleTCPRequest`) | Enables parallel proxying without blocking |
| **UDP session map** | `sync.RWMutex` | Allows concurrent reads (traffic) with exclusive writes (new sessions) |
| **UDP session entry** | `sync.Mutex` (`connLock`) | Prevents races during connection setup and teardown |
| **Client-side** | Identical patterns | [`core/client/client.go`](https://github.com/apernet/hysteria/blob/main/core/client/client.go) mirrors server concurrency logic for bidirectional consistency |

## Summary

- Hysteria accepts QUIC connections in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) and immediately spawns a goroutine per connection via `go s.handleClient(conn)`.
- Each TCP stream within a connection runs concurrently through `go h.handleTCPRequest(qStream)`, preventing head-of-line blocking.
- UDP traffic uses a centralized `udpSessionManager` with a goroutine-per-session model and `sync.RWMutex` for the session map to optimize read-heavy workloads.
- Per-session `sync.Mutex` locks and a dedicated `idleCleanupLoop` ensure safe resource cleanup and prevent race conditions during session termination.

## Frequently Asked Questions

### How many goroutines does Hysteria create per client connection?

Hysteria creates one goroutine to handle the overall QUIC connection, plus an additional goroutine for every active TCP stream via `handleTCPRequest`. For UDP traffic, it spawns one goroutine per UDP session entry to manage read/write loops independently.

### Why does Hysteria use `sync.RWMutex` specifically for UDP sessions?

The `sync.RWMutex` in [`core/server/udp.go`](https://github.com/apernet/hysteria/blob/main/core/server/udp.go) optimizes for read-heavy access patterns. Since normal traffic requires frequent lookups in the session map while new session creation is relatively rare, read locks allow multiple goroutines to query the map simultaneously without blocking each other.

### What prevents race conditions when closing UDP sessions?

Each `udpSessionEntry` maintains its own `sync.Mutex` (the `connLock` field) to serialize connection creation and closure operations. Additionally, the `idleCleanupLoop` checks atomic timestamps before removing entries, ensuring that active sessions are never terminated mid-transmission.

### Does Hysteria reuse goroutines or create new ones for each stream?

Hysteria creates new goroutines for each TCP stream rather than using a fixed worker pool. This approach leverages Go’s efficient scheduler and minimizes latency by eliminating queueing overhead, which is critical for maintaining low-latency proxy performance under high concurrency.