Inside frp’s Tunnel Lifecycle: Login, Work Connections, and Bytes
The central mechanism in frp is not a single tunnel carrying every request. It is a managed session that separates control from payload: frpc logs in to frps, declares a proxy, and then supplies request-specific work connections when public demand arrives. Following that lifecycle explains why frp can serve several local services through one client relationship, why a correctly configured proxy may still wait or fail, and where frps stops interpreting a request and begins joining byte streams.
Login creates a managed client session
A public request cannot establish the relationship that frp needs. Before a user can reach a local service, frpc must first connect to frps and identify the client session that will own the proxy. The login message carries more than an address: it can include the client version, hostname, operating system and architecture, configured user, client ID, metadata, a timestamp, and the number of work connections the client wants prepared. It can also carry a previous run identifier when the client is reconnecting.
That information gives frps a durable subject to manage. The server verifies the login, assigns a run identifier when a new session has none, and registers the control connection. It then returns a login response containing the run identifier. A run identifier is not a public port and does not identify a local service; it identifies a particular client generation so later connections can be attached to the right control state.
The distinction becomes important during reconnection. A client that loses its control connection may return with the previous run identifier. The server can replace the old control generation rather than letting two sessions with the same identity continue to receive work unpredictably. The lifecycle is therefore not “connect once and forget.” It includes ownership, handoff, and cleanup. A stale control connection must not remain eligible for new work simply because it was once valid.
Authentication is part of this transition, not a substitute for it. In the token mode, the client derives a timestamped login key from the configured token and the server compares the result. OIDC is another supported path. Either way, successful authentication allows the control session to be registered; it does not by itself publish a local service or authorize every future user connection. Those are later decisions in the lifecycle.
Once the control session exists, frpc sends proxy definitions. A proxy definition tells frps what kind of public entry point to create and which local endpoint the client is willing to represent. The server validates the definition, reserves or registers the corresponding public listener or route, and returns a response describing the resulting public address. At this stage no user payload has crossed the system. The session has created an offer of reachability, not yet carried a request.
The control connection remains active while the proxy is in use. Heartbeat messages let the two sides test liveness, and the server can close a control session after the configured heartbeat timeout. When that happens, its proxies and queued work connections are cleaned up. A public listener that appears configured but has lost its client relationship is not an independent path to the local service; its ability to forward depends on the control state that owns it.
It helps to view registration as a sequence of ownership changes. Before login, a new connection is only a candidate and has no right to represent a client. After authentication and registration, the control session owns a run identifier and can receive instructions. After proxy registration, the session also owns public routes or listeners. When a work connection is accepted, that capacity is temporarily transferred to a specific proxy request. On closure, the work connection returns no payload capacity; it is closed, while the control session may remain available to replenish the reserve. If the control session itself closes, the higher-level ownership is removed and the proxies are torn down with it.
This layered ownership explains why frp can let one frpc represent several local services. A single session may declare an SSH proxy, an HTTP proxy, and a UDP proxy, each with its own public semantics, while the control channel coordinates their changes. The public side does not need a separate login for every local port. It needs a way to associate each incoming request with the registered proxy and then obtain work capacity from the client session that owns it.
Control messages are not payload streams
The separation between control and payload is the key to understanding frp’s design. The control connection carries instructions and state changes: login, proxy creation, heartbeat, requests for more work connections, and responses to those requests. A work connection carries the traffic associated with a particular forwarded connection or service operation. The two kinds of connection may share an underlying physical transport when multiplexing is enabled, but they have different logical jobs.
This separation lets one client session manage several proxies without placing all their bytes in one undifferentiated stream. frps can ask for capacity, select a proxy, and communicate the selection to the client while the payload is handled separately. Concurrent control operations can also share the control channel through message dispatch keyed to the expected response, rather than requiring a new management session for each operation.
Imagine an SSH connection and an HTTP request arriving close together. They should not be treated as one conversation simply because both entered the same public server. Each request needs a work path associated with the correct proxy, while the control channel can carry the allocation messages for both. This is the point of the message dispatcher and its response matching: control traffic can remain concurrent and typed even when several proxy handlers are active. The design keeps management state legible without requiring every payload to own a separate long-lived control relationship.
There is another useful boundary here. The control channel can describe a source address, a destination address, a proxy name, or an authentication result, but those descriptions are not themselves proof that the local service is healthy. The client still has to connect to the configured endpoint. A successful login can coexist with a refused local port; a successful proxy registration can coexist with a later timeout. frp’s lifecycle makes these outcomes distinct instead of collapsing them into one vague “tunnel failed” condition.
The distinction is practical. If a control message says that a work connection should serve the SSH proxy, it does not contain the SSH conversation. It identifies the relationship and the next transition. The local service’s bytes begin only after the client has accepted the work connection, connected it to the appropriate endpoint, and joined the two sides. This is similar to a dispatcher assigning a transport resource: the assignment is not the cargo.
The same model applies when the proxy is not raw TCP. An HTTP proxy may inspect a public host or URL path before deciding which backend configuration should receive a request, but the resulting connection still needs a work path to the local service. The lifecycle supplies that path; the proxy type determines how much meaning frps attaches to the ingress before the bytes are joined.
A public request consumes work capacity
Now consider one SSH connection arriving at a public TCP listener. frps accepts the user connection and asks the client-side control state for a work connection. If one is already available in the per-client pool, the server takes it and immediately asks frpc to prepare a replacement. If the pool is empty, frps sends a ReqWorkConn message and waits for a new connection to arrive, subject to the server’s user-connection timeout.
The client handles that request by opening another connection to frps. It sends a NewWorkConn message containing the current run identifier and, when configured, the corresponding authentication material. The server uses the run identifier to find the active control session and checks that the work connection uses the negotiated wire protocol and valid authentication context. A work connection from an old, unknown, or mismatched session is not silently attached to a proxy.
After the server accepts the work connection, it sends StartWorkConn. This message names the proxy that should receive the connection and can carry source and destination address information for features that need it. The client dispatches the work connection to the matching proxy handler. That handler connects to the local SSH service, or invokes the relevant local plugin, and then joins the work connection to the local endpoint. On the server side, frps joins the public user connection to the same work path. The result is a bidirectional byte stream from the Internet user to the local service, but it exists because several state transitions succeeded in order.
The source and destination details are useful because the work path can carry more than an undifferentiated byte count. Features such as proxy protocol or traffic metrics may need connection identity or direction information, while an HTTP route may already have chosen a backend based on host and path. The important limit is that these details guide a registered proxy; they do not turn frps into a general-purpose scanner of the private network. The client-side handler still decides whether the configured local endpoint can be opened, and the server-side handler still owns the public connection that must be joined.
For a long-lived SSH session, the work connection remains occupied for as long as the user’s connection remains open. For many short HTTP requests, a proxy may repeatedly consume and replenish work capacity. These workloads stress different parts of the same lifecycle: long-lived sessions test connection stability and cleanup, while bursts test how quickly the reserve can be replenished and how many logical streams the selected transport can carry. The control model is shared, but the operational consequences are not.
The sequence shows why StartWorkConn is a meaningful boundary. Before it, the system is negotiating identity and assigning capacity. After it, the selected handler can perform the local connection and data join. frps may still interpret protocol-specific ingress, apply per-proxy wrapping, or record traffic statistics, but it is no longer deciding which arbitrary private service to scan or discover. The registered proxy contract limits the destination.
Why a configured proxy can still wait
A proxy definition is necessary but not sufficient for immediate service. The client may be offline, the control session may have failed its heartbeat, the local endpoint may reject a connection, or all available work connections may be occupied. When no work connection is ready, frps can request one and wait; if the client cannot provide it before the user-connection timeout, the public request fails even though the public listener and configuration still exist.
Pre-established work connections reduce this setup delay for workloads with short or frequent connections. They also consume resources and are bounded by the server’s pool policy. After a pooled connection is handed to a request, the server asks for a replacement, so the pool is a moving reserve rather than a permanent set of reusable payload streams. A connection that has been assigned to one user operation is not simply handed to a second operation while the first is still using it.
The pool is logical, not necessarily a count of independent TCP sockets visible on the wire. With TCP stream multiplexing, several logical streams can share an underlying connection. QUIC likewise supplies streams over a UDP-based transport. The lifecycle still has distinct work-connection objects and allocation decisions, but the physical transport depends on configuration. This distinction prevents two opposite mistakes: assuming every request creates a brand-new network handshake, or assuming one physical connection means every request shares one inseparable payload stream.
Failure also propagates through ownership. If the control dispatcher exits, the client closes its proxies and visitors; the server marks the client offline and closes queued work connections. If StartWorkConn cannot be delivered or the selected work connection fails, the server can discard that connection and try another available one within the proxy’s allocation logic. These are not exotic edge cases. They are the normal consequences of making public reachability depend on a private-side session that can disappear.
A reconnect can restore the relationship, but it is not the same as preserving every in-flight conversation. The new control generation can reclaim the client identity and recreate configured proxies, while a work connection that was already closed has lost its byte stream. This is a useful operational distinction: configuration persistence can restore the ability to accept new requests, but it cannot retroactively restore bytes that were being carried through a failed connection. Applications that need retry or session recovery must provide it at their own protocol layer.
The lifecycle gives proxy modes a common shape
At this point, the different frp modes can be seen as variations on one lifecycle rather than as unrelated products. TCP changes which public port and local stream are joined. UDP uses a datagram-oriented path. HTTP and HTTPS add host, path, and header decisions before selecting the local connection. A private visitor mode changes who may create the user-side connection and avoids the same kind of public remote listener. A direct-path mode changes where the payload may travel after coordination, but still relies on the client identity, proxy registration, and work signaling that make the attempt meaningful.
That common shape is useful because it locates the next design question. First ask how the request is carried; then ask what the request means. A raw TCP proxy and an HTTP proxy may both consume a work connection, but one treats the payload as an opaque stream while the other uses public request metadata to choose a route. The next step is to examine those exposure contracts—and the different audiences they allow—without losing sight of the lifecycle that carries them.