If you only need a stable public URL for webhooks or a demo, Cloudflare Tunnel on a domain you own is the best free ngrok replacement in 2026, and frp on a $4 VPS is the best one you fully control. ngrok's free plan is now $0 with 1 GB of data transfer out, 20,000 HTTP requests a month, 3 online endpoints and a single dev domain[1]. That is enough for a weekend of Stripe testing and not enough for a home-lab service. Tailscale Funnel is the fastest one-liner but limits you to your ts.net name. rathole and bore are TCP-level tools, sish is SSH with HTTP routing, and Pangolin is a full zero-trust platform. Below: one comparison table, three working recipes, the webhook pattern, security rules, and when paying ngrok is the right call.
TL;DR
Need HTTP on your own hostname, free, no VPS: Cloudflare Tunnel (needs a domain on Cloudflare).
Need it in 30 seconds with no domain:
tailscale funnel 3000or a Cloudflare quick tunnel. Both change what the URL looks like, and quick tunnels are for testing only.Need raw TCP or UDP, or full control: frp on a small VPS, or rathole for a lighter Rust binary.
Need identity-aware access, SSO and per-resource rules: Pangolin, self-hosted or cloud.
Pay ngrok when the tunnel is a work tool for a team and an hour of your time costs more than $10.
What ngrok's free plan actually gives you in October 2026
The free plan includes $5 of one-time usage credit, then hard quotas[1][2]:
1 GB data transfer out and 20,000 HTTP requests per month
5,000 TCP connections a month, with a rate cap of 100 TCP connections per minute
3 online endpoints, 3 concurrent agents, 1 user
One automatically assigned development domain, no custom domains
No TLS endpoints, and an interstitial warning page shown to browser traffic
The interstitial is the part that bites demos. Visitors click "Visit" once, which sets a cookie for 7 days. API callers such as webhook senders are not affected, and you can skip it with the ngrok-skip-browser-warning header[2]. Custom domains start on Pay-as-you-go at $20 a month plus usage. Hobbyist is $10 a month[1].
One more reason to leave: Razorpay's webhook docs say many common tunneling services are blacklisted for delivery and suggest zrok instead[9]. A tunnel on your own domain sidesteps that.
The comparison
| Tool | You need | TLS | Custom domain | Protocols | Auth in front | Free-tier limits | Setup | Upkeep |
|---|
| ngrok Free | Account | Managed | No (one dev domain) | HTTP/S, TCP | Traffic policy, 3 OAuth MAU | 1 GB, 20k req, 3 endpoints | Minutes | None |
| Cloudflare Tunnel | Free account + domain on Cloudflare | Managed at Cloudflare edge | Yes | HTTP, SSH, RDP and other protocols per docs | Cloudflare Access policies, service tokens | No bandwidth quota stated in the docs I read | 10 minutes | Keep cloudflared updated |
| Cloudflare quick tunnel | Nothing | Managed | No, random hostname | HTTP | Optional email allowlist | 200 in-flight requests, no SSE, no uptime guarantee | 10 seconds | None, not for production |
| Tailscale Funnel | Tailscale account, MagicDNS, HTTPS on | Managed certs | No, tailnet.ts.net names only | HTTPS, TCP with TLS | Tailscale ACL controls who can enable it | Non-configurable bandwidth limits; ports 443, 8443, 10000 | 2 minutes | Low |
| frp | VPS + domain | You (Caddy or similar) | Yes | TCP, UDP, HTTP, HTTPS, P2P mode | Token between client and server; HTTP auth is your proxy | Your VPS bandwidth | 30 minutes | Patch VPS and frp |
| rathole | VPS | Optional TLS or Noise transport | n/a, TCP-level | TCP, UDP; HTTP vhosts not documented | Per-service token | Your VPS bandwidth | 20 minutes | Low |
| bore | VPS (or bore.pub) | None by default | n/a | TCP only | Optional shared secret (handshake only) | Your VPS bandwidth | 5 minutes | Low |
| sish | VPS + domain | Flag-enabled HTTPS, certs from a directory | Yes | HTTP/WS, TCP, TCP aliases, SNI | SSH keys or password | Your VPS bandwidth | 30 minutes | Medium |
| Pangolin | VPS (self-host) or Pangolin Cloud | Managed by the stack | Yes | HTTPS proxy, plus SSH, RDP, VNC and private ranges | Built-in SSO, roles, PIN, email OTP, geoblocking | Community Edition AGPL-3, free; Enterprise free under $100K revenue[8] | 1 hour | Highest |
Sources for the rows: Cloudflare[3][4], Tailscale[5], frp[6], rathole, bore and sish READMEs[7], Pangolin[8]. I did not benchmark latency. Every tool in the table adds a hop, and a tunnel that terminates at a nearby edge or VPS will feel faster than one routed across a continent. Test with your own region.
Recipe 1: Cloudflare Tunnel with a named tunnel and config.yml
You need a domain whose DNS is on Cloudflare and cloudflared installed (brew install cloudflared on macOS; the image is cloudflare/cloudflared on Docker Hub[3]). The connector opens outbound connections only, so you can block all inbound traffic at the firewall[4]. Allow outbound port 7844 to Cloudflare[3].
# 1. Authenticate this machine
cloudflared tunnel login
# 2. Create the tunnel (writes a UUID.json credentials file)
cloudflared tunnel create dev-tunnel
# 3. Point a hostname at it
cloudflared tunnel route dns dev-tunnel hooks.example.com
# 4. Run it
cloudflared tunnel run dev-tunnel
Put the settings in ~/.cloudflared/config.yml so step 4 needs no flags. The documented format is a url for the origin, the tunnel UUID and the credentials file[3]:
url: http://localhost:3000
tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef
credentials-file: /root/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json
Replace the UUID with the one printed in step 2. If the file lives elsewhere, add --config /path/to/config.yml before run. For a no-account test, cloudflared tunnel --url http://localhost:8080 starts a quick tunnel. Cloudflare's docs call those "for testing and development", with a 200 in-flight request cap, no Server-Sent Events and a hostname that changes every run[3]. Do not point a payment provider at one.
Recipe 2: frp on a $4 VPS with TLS from Caddy
frp supports TCP, UDP, HTTP and HTTPS forwarding by domain[6]. The layout: frps listens for clients on 7000 and for HTTP on 8080. Caddy owns 80 and 443, gets a certificate, and proxies to 8080. Close 8080 in the firewall so only Caddy reaches it. Point an A record for hooks.example.com at the VPS first.
# /etc/frp/frps.toml (on the VPS)
bindPort = 7000
vhostHTTPPort = 8080
auth.method = "token"
auth.token = "replace-with-a-long-random-string"
# frpc.toml (on your laptop)
serverAddr = "203.0.113.10"
serverPort = 7000
auth.method = "token"
auth.token = "replace-with-a-long-random-string"
[[proxies]]
name = "webhooks"
type = "http"
localPort = 3000
customDomains = ["hooks.example.com"]
# /etc/caddy/Caddyfile (on the VPS)
hooks.example.com {
reverse_proxy 127.0.0.1:8080
}
Start with frps -c /etc/frp/frps.toml and frpc -c frpc.toml, then sudo ufw allow 80,443,7000/tcp and nothing else public. The client token is the same value on both sides[6]. The Caddy block is my config, not from the frp docs, and Caddy fetches and renews the certificate on its own. Add basic_auth inside the block if the endpoint is a human-facing dashboard. For a quick sanity check, run curl -i https://hooks.example.com/health with your app up.
Recipe 3: Tailscale Funnel in one line
Funnel needs Tailscale 1.38.3 or later, MagicDNS and HTTPS certificates enabled for the tailnet, and a funnel node attribute in the policy file. It is included on all plans and still labeled beta[5]. The first run walks you through enabling it.
tailscale funnel 3000
The public name is under your tailnet-name.ts.net domain, on port 443, 8443 or 10000 only, and traffic must be TLS. Bandwidth limits exist and you cannot change them[5]. By default every tailnet member can use Funnel; Owners, Admins and Network admins can restrict it with the funnel attribute. A port cannot serve privately and publicly at once, so the latest of serve and funnel wins. You cannot bring hooks.example.com, which makes Funnel a good demo tool and a mediocre webhook endpoint if your provider wants an allowlisted domain.
The webhook pattern: one stable URL, verify the signature, replay
Stripe, Razorpay and GitHub all need a public URL, and they retry. Stripe tells you to return a 200 quickly and do slow work asynchronously[10]. The pattern that survives restarts:
Run a named tunnel or frp host so the URL never changes. Register it once in each dashboard.
Verify the signature on the raw body, never the parsed JSON. Stripe sends
Stripe-Signature[10], GitHub sendsX-Hub-Signature-256as an HMAC-SHA256 hex digest prefixed withsha256=[11], and Razorpay sendsX-Razorpay-Signatureand says not to parse the body before verifying[9].Keep payloads from failed deliveries and replay them at localhost. Stripe's CLI can also skip the tunnel entirely:
stripe listen --forward-to localhost:4242/webhookandstripe trigger payment_intent.succeeded[10].Rebuild a failed request as code with the cURL to code converter and fire it at your handler.
A tunnel gives your laptop a public face, so treat the path as hostile. A public webhook route accepts POSTs from anyone, and only the signature check separates Stripe from a stranger.
Security notes before you expose anything
Expose one route, not the machine. Point the tunnel at the webhook handler's port, not at a dev server that also serves an admin panel, a debugger or
/_nextinternals.Never tunnel an unauthenticated admin UI. Put identity in front. With Cloudflare, an Access policy denies everyone by default unless an Allow policy matches. Service Auth with a service token covers non-human callers[12]. Tailscale restricts who can switch on Funnel. Pangolin gives per-resource roles, PIN codes and email OTP[8].
Rate-limit at the edge or the proxy. Caddy needs a plugin for it; Cloudflare has rules on its side. Without a cap, a bot can fill your tunnel and your logs.
Treat bore's secret as a handshake lock only. The README says nothing beyond the handshake is encrypted by default[7]. Run TLS inside the tunnel or choose a different tool.
Rotate tokens. A leaked frp token lets anyone register proxies on your server.
Kill it when done. A tunnel left running over a weekend is an open door. Run tunnels through
systemdor a Compose service you can stop; the Docker Compose production guide shows the pattern.
Using a tunnel to reach an agent's callback or an MCP server raises the stakes, since the caller can trigger actions. The Claude Managed Agents tunnels guide covers that case.
Picking one, and when to just pay ngrok
Webhook testing on a domain you own: Cloudflare Tunnel. A TCP service such as SSH, Postgres or a game server: frp or rathole on a VPS. A one-off demo to a colleague: Funnel or a quick tunnel. Zero-trust access for a family or team homelab: Pangolin, accepting the heavier install.
Pay ngrok when setup time costs more than the plan. Hobbyist at $10 a month removes the interstitial and fits one person doing steady client demos[1]. Pay-as-you-go at $20 plus usage buys custom domains for a team that does not want to run a VPS[1]. I would pay before I ran frp for a team of five who rarely touch infrastructure.
One alternative needs no tunnel at all: if the webhook target is a bot or a small API, deploy it as a Worker and register the Worker URL. The personal AI agent on the Cloudflare free tier post does this for a Telegram webhook, and the Cloudflare Workers guide covers the deployment side.
Quick answers
Is Cloudflare Tunnel really free?
The tunnel docs I read do not state a price or a bandwidth quota, and the pricing page did not load, so check the current Zero Trust plan before relying on it. You do need a domain on Cloudflare for named tunnels. Quick tunnels need no account but are limited to testing.
Can Tailscale Funnel use my own domain?
No. Funnel only serves DNS names under your tailnet domain, tailnet-name.ts.net, on ports 443, 8443 and 10000.
Which tool handles UDP?
frp lists UDP support, and rathole supports UDP services with type = "udp" on the client. bore is TCP only. Cloudflare's tunnel overview does not mention UDP, so test before assuming it.
What is the cheapest way to get a stable webhook URL?
A named Cloudflare Tunnel on a domain you already own costs nothing beyond the domain. If you want the box under your control, frp plus Caddy runs on a $4 VPS.
Do I still need a tunnel for Stripe testing?
Not for development. stripe listen --forward-to pushes events to localhost without a public URL. You need a public endpoint when a provider has no CLI forwarder, as with GitHub or Razorpay.
Sources
Cloudflare: create a remotely-managed tunnel (accessed 16 October 2026)
Razorpay: validate and test webhooks (accessed 16 October 2026)
Stripe: set up and deploy a webhook (accessed 16 October 2026)
GitHub: validating webhook deliveries (accessed 16 October 2026)
12. Cloudflare Access policies (accessed 16 October 2026)
Originally published at wowhow.cloud
Top comments (0)