How FRP Connection Pooling Works and When to Enable It

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. 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.

// 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.

// 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.

// 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 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.

// 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, the client advertises its desired pool size to the server.

// 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 establishes a fresh TCP connection to the server, authenticates it, and dispatches it to the appropriate proxy manager.

// 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 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.


# frpc.yaml

transport:
  poolCount: 3

Corresponding source: 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.


# frps.yaml

transport:
  maxPoolCount: 10

Corresponding source: 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.

// 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, 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 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.

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 →