Introduction to ngrok Alternatives
ngrok has long been the go-to tool for exposing local development servers to the internet via secure tunnels. However, as projects scale and requirements diversify, many developers seek alternatives such as Cloudflare Tunnel (cloudflared), localtunnel, Expose, Bore, FRP (Fast Reverse Proxy), and Serveo. While these tools solve similar problems, each comes with its own quirks, configuration models, and failure modes.
This tutorial walks through the most common issues developers encounter when adopting an ngrok alternative and provides actionable fixes, configuration examples, and best practices to keep your tunnels reliable.
Why Choosing the Right ngrok Alternative Matters
Switching tunneling tools is not just about preference. The right alternative can impact:
- Latency and throughput — some self-hosted options outperform SaaS tunnels for regional traffic.
- Cost — many alternatives are fully open source and free for unlimited tunnels.
- Privacy and compliance — self-hosted solutions keep traffic within your infrastructure.
- Custom domains — some tools make it easier to bind your own domain without paywalls.
- Protocol support — TCP, UDP, and SSH tunneling support varies widely.
Understanding where these tools fail helps you avoid downtime during demos, webhook testing, and mobile device QA.
Common Issues and How to Fix Them
1. Tunnel URL Returns 502 Bad Gateway
A 502 error means the tunnel server reached your local machine, but the local service did not respond correctly. This is the single most common issue across all ngrok alternatives.
Common causes:
- Your local server is bound to
127.0.0.1instead of0.0.0.0. - The port specified does not match the running service.
- The service crashed or is still starting up.
Fix: Ensure your dev server listens on all interfaces. For example, with Vite:
# Incorrect - only accessible from localhost
vite --host 127.0.0.1 --port 3000
# Correct - accessible from the tunnel
vite --host 0.0.0.0 --port 3000
For a Node.js Express server:
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello from tunnel'));
// Bind to 0.0.0.0 so the tunnel can reach it
app.listen(3000, '0.0.0.0', () => {
console.log('Server running on 0.0.0.0:3000');
});
2. Cloudflare Tunnel Fails to Authenticate
When using cloudflared, the tunnel login step sometimes hangs or fails with a certificate error.
Fix: Manually download the certificate and place it in the config directory:
# Authenticate interactively
cloudflared tunnel login
# If it hangs, download cert.pem from the Cloudflare dashboard
# and place it manually
mkdir -p ~/.cloudflared
mv ~/Downloads/cert.pem ~/.cloudflared/cert.pem
# Verify
cloudflared tunnel list
On Windows, the config directory is %USERPROFILE%\.cloudflared.
3. localtunnel Returns Random Subdomains or 404
localtunnel assigns random subdomains by default, which break webhooks that expect a stable URL. It also occasionally returns 404 because the requested subdomain is already in use.
Fix: Always request a specific subdomain and add retry logic:
# Request a fixed subdomain
lt --port 3000 --subdomain my-stable-dev-url
# Programmatic usage with Node.js
const localtunnel = require('localtunnel');
(async () => {
const tunnel = await localtunnel({
port: 3000,
subdomain: 'my-stable-dev-url'
});
console.log('Tunnel URL:', tunnel.url);
tunnel.on('close', () => {
console.log('Tunnel closed');
});
})();
4. FRP Connection Refused or Timeout
FRP requires both a server (frps) and a client (frpc). A common mistake is mismatched authentication tokens or bind ports between the two.
Server config (frps.toml):
bindPort = 7000
auth.token = "super-secret-token"
# Optional dashboard
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "admin123"
Client config (frpc.toml):
serverAddr = "your-server-ip"
serverPort = 7000
auth.token = "super-secret-token"
[[proxies]]
name = "web"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3000
remotePort = 8080
Run both sides:
# On the server
./frps -c frps.toml
# On the client
./frpc -c frpc.toml
If you see a connection refused error, verify the token matches exactly and that port 7000 is open in your firewall.
5. Webhooks Fail to Validate SSL
Many webhook providers (Stripe, GitHub, Slack) require a valid HTTPS endpoint. Some alternatives only provide HTTP unless configured properly.
Fix with Cloudflare Tunnel: Cloudflare automatically provides HTTPS, but you must route through a hostname:
# Create a named tunnel
cloudflared tunnel create my-dev-tunnel
# Configure DNS
cloudflared tunnel route dns my-dev-tunnel dev.example.com
# Run the tunnel
cloudflared tunnel --url http://localhost:3000 run my-dev-tunnel
Now https://dev.example.com routes securely to your local server.
6. Bore Server Drops Connections on Idle
Bore is a lightweight Rust-based tunnel, but its default idle timeout can drop long-lived connections such as WebSockets.
Fix: Run your own Bore server with a higher timeout and keep-alive on the client:
# Self-hosted Bore server
bore server --min-port 1024 --max-port 65535
# Client with keep-alive
bore local 3000 --to your-server.com --port 7835
For WebSocket-heavy apps, add application-level ping frames every 30 seconds to prevent idle disconnects.
7. Expose Authentication Errors with Custom Domains
Expose (by BeyondCode) supports custom subdomains, but misconfigured DNS or missing tokens cause silent failures.
# Start the server with a custom domain
expose share http://localhost:3000 --subdomain=myapp --server=expose.example.com
# If authentication is required
expose share http://localhost:3000 --auth-token=your-token
Ensure your DNS has a wildcard *.expose.example.com record pointing to your Expose server.
Best Practices for Reliable Tunnels
- Pin your versions. Tunneling tools change APIs frequently. Lock versions in your package manager or Docker image.
- Use config files over CLI flags. Config files are reproducible and version-controllable.
- Always bind to 0.0.0.0. This eliminates the most common 502 errors.
- Monitor tunnel health. Add a health check endpoint and alert if the tunnel stops responding.
- Rotate auth tokens. For self-hosted solutions like FRP, rotate tokens regularly.
- Use HTTPS everywhere. Even for local testing, HTTPS surfaces mixed-content issues early.
- Document your tunnel setup. Include tunnel commands in your project README so teammates can replicate the environment.
Here is a sample health check script you can run alongside your tunnel:
#!/bin/bash
# health-check.sh - verify tunnel is alive
TUNNEL_URL="${1:-https://dev.example.com}"
MAX_RETRIES=5
RETRY_DELAY=10
for i in $(seq 1 $MAX_RETRIES); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$TUNNEL_URL/health")
if [ "$HTTP_CODE" = "200" ]; then
echo "Tunnel healthy (HTTP $HTTP_CODE)"
exit 0
fi
echo "Attempt $i: tunnel returned $HTTP_CODE, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
echo "Tunnel is down after $MAX_RETRIES attempts"
exit 1
Conclusion
Troubleshooting ngrok alternatives comes down to understanding each tool's architecture, configuration model, and common failure points. Most issues — 502 errors, authentication failures, unstable subdomains, and SSL validation problems — stem from a small set of root causes that are easy to fix once you know where to look. By binding your servers to 0.0.0.0, using config files, enabling HTTPS, and adding health checks, you can build a tunneling setup that is as reliable as ngrok while giving you greater control, lower costs, and better privacy. Pick the tool that matches your workload, follow the best practices above, and your local-to-public development workflow will stay robust under pressure.