Why frp is the reverse proxy you should know about
You’re building a service locally and you need it on the internet. Maybe a webhook to test. Maybe a demo for someone. Maybe remote access to your home lab. Whatever it is, you’re behind NAT or a firewall, and nothing outside can reach you.
frp (Fast Reverse Proxy) solves this. It’s written in Go, it’s a single binary, and it handles everything from plain HTTP tunnels through to P2P connections. I think it deserves a spot in more Go developers’ toolkits than it currently has.
What makes frp different?
The obvious comparison is ngrok. The difference is that frp is self-hosted: you run the server, you control the data, and no third party sits in the middle of your traffic. If you’re already paying for a cheap VPS, tunnelling effectively comes free with it.
The architecture is refreshingly simple. You run frps (the server) on a machine with a public IP. You run frpc (the client) on your local machine, and it establishes an outbound connection through your firewall. Traffic flows back through that tunnel.
The protocol support is broader than I expected from a tool like this. TCP, UDP, HTTP, HTTPS, STCP (secret TCP), and even P2P connections. One tool, a lot of ground covered.
Setting up a basic HTTP proxy
Let’s expose a local web server. First, the server configuration (frps.toml):
bindPort = 7000
vhostHTTPPort = 80
Start the server:
./frps -c frps.toml
Now the client configuration (frpc.toml):
serverAddr = "your-public-server.com"
serverPort = 7000
[[proxies]]
name = "web"
type = "http"
localPort = 3000
customDomains = ["demo.your-domain.com"]
Start the client:
./frpc -c frpc.toml
Your local port 3000 is now accessible at demo.your-domain.com. That’s the whole setup.
Performance tuning for production
frp behaves well out of the box, but a few settings are worth knowing before you put real traffic through it.
Connection pooling
Opening a new connection per request is expensive. frp can keep a pool of them warm instead:
serverAddr = "your-public-server.com"
serverPort = 7000
transport.poolCount = 5
[[proxies]]
name = "web"
type = "http"
localPort = 3000
customDomains = ["demo.your-domain.com"]
The poolCount setting keeps 5 connections ready to go, so requests skip the handshake entirely.
TCP mux
Multiple proxies can share a single TCP connection:
serverAddr = "your-public-server.com"
serverPort = 7000
transport.tcpMux = true
[[proxies]]
name = "web"
type = "http"
localPort = 3000
customDomains = ["demo.your-domain.com"]
[[proxies]]
name = "api"
type = "http"
localPort = 8080
customDomains = ["api.your-domain.com"]
Both proxies now ride on one underlying connection. Less memory, fewer file descriptors, one fewer thing to break.
Bandwidth limiting
If you’re running this on a small VPS, you probably don’t want one tunnel eating the whole pipe. frp supports limits per proxy:
[[proxies]]
name = "web"
type = "http"
localPort = 3000
customDomains = ["demo.your-domain.com"]
transport.bandwidthLimit = "10MB"
transport.bandwidthLimitMode = "server"
This caps the proxy at 10MB/s. The bandwidthLimitMode can be client or server, depending on where you want to enforce the limit.
P2P mode for low latency
By default, all traffic routes through your server, which means you pay for the bandwidth and the extra round trip. P2P mode connects the two ends directly:
[[proxies]]
name = "p2p-ssh"
type = "xtcp"
secretKey = "your-secret-key"
localIP = "127.0.0.1"
localPort = 22
The visitor configuration:
[[visitors]]
name = "p2p-ssh-visitor"
type = "xtcp"
serverName = "p2p-ssh"
secretKey = "your-secret-key"
bindAddr = "127.0.0.1"
bindPort = 2222
Traffic now flows directly between peers, with the server only brokering the initial handshake. It won’t work everywhere, since some NAT setups defeat the hole punching, but when both machines can establish a direct connection it’s a noticeable win for latency-sensitive things like SSH.
Monitoring your setup
Tunnels fail quietly, so you want visibility into what’s actually happening. frp exposes a dashboard for exactly this. Enable it in the server config:
bindPort = 7000
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "secure-password"
Access it at http://your-server:7500 and you’ll see connected clients, active proxies, and traffic stats. And if the service behind the tunnel is your own Go code, make sure it behaves well under cancellation and timeouts too. How to use context properly covers the patterns you’ll want for request tracing.
When to use frp
I’d use frp for development environments that need public access, home lab services you want to reach securely, IoT devices stuck behind restrictive networks, and backup access to internal services. What I wouldn’t do is treat it as a stand-in for real infrastructure. Production traffic deserves a proper load balancer.
But for punching through a NAT or firewall to reach something you own? frp does the job well, and the Go implementation keeps it lightweight. One binary, no runtime dependencies, config you can read in one sitting. Deploying it takes about as long as reading this post did.