A localhost tunnel gives a server running on your machine or a private network a public URL. An agent on your side opens an outbound connection to a tunnel service, and the service relays any request sent to the public hostname back down that connection to your local port. Your server becomes reachable from the internet without a public IP, port forwarding or firewall changes.
relay connect --name my-app http://localhost:3000
# prints a public HTTPS URL that forwards to http://localhost:3000
How a tunnel works
- You start your app on a local port, for example
localhost:3000. - You run the tunnel agent. It makes an outbound TLS connection to the tunnel service. Outbound connections pass through NAT and most firewalls, so nothing inbound needs to be opened.
- The service assigns a public hostname and terminates HTTPS for it.
- A browser or API client requests the public URL. The service forwards the request over the open connection.
- The agent sends it to
localhost:3000, takes the response and streams it back the same way.
In effect, the tunnel service is a reverse proxy on the internet whose upstream happens to be your laptop.
browser ──HTTPS──▶ tunnel service (public hostname, TLS)
▲
│ outbound connection opened by the agent
│
your machine: tunnel agent ──▶ http://localhost:3000
What tunnels are used for
- Showing work in progress to a client or teammate without deploying.
- Testing on real devices. Open your local site on a phone over HTTPS.
- Callbacks and OAuth redirects that need a public HTTPS URL. See callback URL.
- Receiving webhooks during development.
- Reaching a service with no public IP, such as a home server, a Raspberry Pi or an internal dashboard.
- TCP services like SSH or a database, with a TCP tunnel instead of HTTP.
Tunnel vs webhook forwarding
For webhooks specifically, webhook forwarding is often the better tool:
| Tunnel | Webhook forwarding | |
|---|---|---|
| Exposes | The whole port, any path | Only webhook requests to one URL |
| Machine offline | Requests fail (usually a 502 or 504) | Requests are stored and retried or replayed |
| One URL, many destinations | No | Yes |
| Interactive browsing | Yes | No |
Webhook Relay offers both: relay connect for tunnels and relay forward for webhook forwarding.
Common pitfalls
- The URL changes on every restart. Many free tunnel tools assign a random hostname each time, so every webhook or OAuth setting must be updated. Use a tool that keeps a stable name. With Webhook Relay, reusing
--namekeeps the same address. - Host header mismatches. Dev servers like Vite and webpack reject requests whose
Hostis notlocalhost("Invalid Host header" or "Blocked request"). Allow the tunnel hostname in the dev server config, or have the tunnel rewrite the Host header. - Certificate errors. If the tunnel forwards to
https://localhostwith a self-signed certificate, the agent may refuse it. Point the tunnel at the plainhttp://port and let the service terminate public TLS. A self-signed certificate exposed directly to browsers causesERR_CERT_AUTHORITY_INVALID. - Binding to the wrong interface. If the agent runs in Docker,
localhostmeans the container. Use the host's address or the service name. - Leaving it open. A tunnel to a dev server with debug endpoints is a public attack surface. Add authentication and stop it when you're done.
- Latency. Traffic goes via the tunnel service. Pick the region closest to your users; Webhook Relay tunnels run in multiple regions.
See tunnels for setup, or demoing your website for a walkthrough.
