# How FRP Connection Pooling Works and When to Enable It

> Discover how frp connection pooling works to reduce latency. Learn when to enable pool_count and max_pool_count for efficient TCP connections between frp client and server.

- Repository: [fatedier/frp](https://github.com/fatedier/frp)
- Tags: internals
- Published: 2026-02-26

---

**FRP maintains a pool of pre-established TCP work connections between the client and server to eliminate connection setup latency, configured via `pool_count` on the client and capped by `max_pool_count` (default 5) on the server.**

FRP (Fast Reverse Proxy), implemented in the **fatedier/frp** repository, separates control plane signaling from data plane traffic using distinct TCP connections. **Connection pooling** allows the client (`frpc`) to pre-establish multiple work connections that the server (`frps`) stores in a buffered channel, reducing per-request overhead. This mechanism is particularly beneficial for high-throughput proxies or high-latency networks, though it consumes additional file descriptors on both ends.

## Server-Side Pool Lifecycle

The server manages pooled connections through the `Control` object in [`server/control.go`](https://github.com/fatedier/frp/blob/main/server/control.go). When a client initiates a session, the server creates a buffered Go channel (`workConnCh`) sized to accommodate the requested pool plus a burst buffer.

### Connection Cap and Channel Initialization

During login, the server reads the client's desired `PoolCount` from the `Login` message and enforces its own `MaxPoolCount` limit to prevent resource exhaustion.

```go
// server/control.go L71-L74
poolCount := loginMsg.PoolCount
if poolCount > int(serverCfg.Transport.MaxPoolCount) {
    poolCount = int(serverCfg.Transport.MaxPoolCount)
}

```

The control object then initializes the channel with extra capacity for burst traffic.

```go
// server/control.go L83-L86
workConnCh: make(chan net.Conn, poolCount+10),
poolCount: poolCount,

```

### Warm-Up and Replenishment Logic

Immediately after establishing the control connection, the server sends `ReqWorkConn` messages to request the client open the specified number of work connections, filling the pool before any user traffic arrives.

```go
// server/control.go L19-L22 (inside Start)
for i := 0; i < ctl.poolCount; i++ {
    _ = ctl.msgDispatcher.Send(&msg.ReqWorkConn{})
}

```

When a proxy needs to forward traffic, `GetWorkConnFromPool` in [`server/proxy/proxy.go`](https://github.com/fatedier/frp/blob/main/server/proxy/proxy.go) pulls a connection from the channel. If the pool is empty, the system falls back to requesting a new connection and blocks until it arrives. Critically, the server replenishes the pool immediately after handing out a connection to maintain steady-state capacity.

## Client-Side Pool Lifecycle

The client maintains the pool by responding to the server's `ReqWorkConn` requests and configuring the initial pool size via transport settings.

### Configuration and Login

The default `PoolCount` is `1`, defined in the client's transport configuration structure.

```go
// pkg/config/v1/client.go L16-L19
// PoolCount specifies the number of connections the client will make to the server in advance.
PoolCount int `json:"poolCount,omitempty"`

```

During the initial handshake in [`client/service.go`](https://github.com/fatedier/frp/blob/main/client/service.go), the client advertises its desired pool size to the server.

```go
// client/service.go L86-L91
loginMsg := &msg.Login{
    …
    PoolCount: svr.common.Transport.PoolCount,
}

```

### Handling Work Connection Requests

When the server requests additional work connections via `ReqWorkConn`, the client's `handleReqWorkConn` function in [`client/control.go`](https://github.com/fatedier/frp/blob/main/client/control.go) establishes a fresh TCP connection to the server, authenticates it, and dispatches it to the appropriate proxy manager.

```go
// client/control.go L25-L60 (handleReqWorkConn)
workConn, err := ctl.connectServer()
…
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)

```

## When to Enable and Tune FRP Connection Pooling

Adjusting the pool size trades resource consumption (file descriptors, memory) against connection latency. Use the following guidelines based on workload characteristics and network conditions.

**High-throughput or low-latency workloads** (HTTP APIs, SSH sessions, many short-lived connections): Increase `pool_count` on the client and raise `max_pool_count` on the server. This eliminates the round-trip delay of negotiating a new TCP connection for every request.

**High network latency** (cross-region, satellite, or international links): Larger pools provide significant performance benefits because the expensive TCP handshake and TLS negotiation occur before user traffic arrives, masking network round-trip time.

**Resource-constrained environments** (embedded devices, limited file descriptors): Retain the default settings (`pool_count = 1`, `max_pool_count = 5`). Larger pools maintain idle connections that consume file descriptors on both ends and may trigger "connection pool is full" warnings under heavy load.

**Bounding maximum connections**: Server administrators should set `max_pool_count` in [`frps.ini`](https://github.com/fatedier/frp/blob/main/frps.ini) to hard-limit resource usage per client. The server automatically caps any client-requested pool size that exceeds this threshold.

## Configuration Examples

### Client Configuration

Set `pool_count` in the common section of the client configuration file to pre-establish three work connections.

```yaml

# frpc.yaml

transport:
  poolCount: 3

```

Corresponding source: [`pkg/config/v1/client.go`](https://github.com/fatedier/frp/blob/main/pkg/config/v1/client.go) defines the `PoolCount` field.

### Server Configuration

Set `max_pool_count` to limit the maximum pool size any single client can request.

```yaml

# frps.yaml

transport:
  maxPoolCount: 10

```

Corresponding source: [`pkg/config/v1/server.go`](https://github.com/fatedier/frp/blob/main/pkg/config/v1/server.go) contains the `MaxPoolCount` field.

### Internal Pool Fetching Logic

The proxy implementation attempts to retrieve connections from the pool with a fallback to on-demand creation if the buffer is exhausted.

```go
// server/proxy/proxy.go L22-L32
for i := 0; i < pxy.poolCount+1; i++ {
    if workConn, err = pxy.getWorkConnFn(); err != nil { … }
}

```

## Summary

- **Pooling is optional**: FRP functions correctly with a single work connection; pooling merely reduces latency by pre-establishing connections.
- **Server-enforced limits**: The `max_pool_count` setting protects the server from excessive resource consumption regardless of client requests.
- **Dynamic fallback**: If the pool is exhausted, FRP automatically requests additional connections on demand, ensuring traffic never stalls permanently.
- **Resource trade-off**: Each pooled connection maintains an open TCP socket and goroutine; increase pool sizes only when network latency or connection churn justifies the overhead.

## Frequently Asked Questions

### What is the default pool size in FRP?

The default `pool_count` on the client is `1`, and the default `max_pool_count` on the server is `5`. This ensures basic functionality without excessive resource usage in standard deployments.

### Can the client exceed the server's maximum pool count?

No. According to [`server/control.go`](https://github.com/fatedier/frp/blob/main/server/control.go), the server caps the client's requested `PoolCount` at its configured `MaxPoolCount` during the login phase. The client cannot force the server to maintain more connections than permitted by server policy.

### Does FRP require connection pooling to function?

No. Pooling is an optimization, not a requirement. With `pool_count` set to `0` or `1`, FRP creates work connections on demand when user traffic arrives. The system includes fallback logic in [`server/proxy/proxy.go`](https://github.com/fatedier/frp/blob/main/server/proxy/proxy.go) to request new connections dynamically if the pool is empty.

### What happens when the connection pool is exhausted?

When the pool is empty, the proxy calls `getWorkConnFn()` which blocks until a connection becomes available or the server successfully requests a new connection from the client via `ReqWorkConn`. The system prioritizes availability over strict pool limits, though high latency during this fallback may impact performance.