localtunnel prints this error when the loca.lt server has already closed the port your client was told to use. It is rarely your firewall. loca.lt closes a tunnel's port about a minute after it stops hearing from your machine, and the lt client never asks for a new port. It reconnects to the old one, gets refused, and exits. A laptop sleep or a network drop that keeps the client offline for more than a minute can be enough. Restart lt with the same --subdomain to try for the same URL, and run it in a restart loop if it has to survive the next sleep.
your url is: https://lc-sleep-16722.loca.lt
.../node_modules/localtunnel/bin/lt.js:81
throw err;
^
Error: connection refused: localtunnel.me:30667 (check your firewall settings)
at Socket.<anonymous> (.../node_modules/localtunnel/lib/TunnelCluster.js:52:11)
The port number changes on every run. Everything below was run on 12 October 2026 from a Mac against the public loca.lt server, with localtunnel 2.0.2, the newest release on npm, published in September 2021 (localtunnel on npm↗). The HOST case at the end was run in a Linux container. An AI assistant helped draft this post. The commands and numbers come from my own runs.
What the error means
localtunnel works like most tunnels (the local tunneling guide covers the general model). When you run lt --port 3000, the client makes a single HTTPS request to https://localtunnel.me/?new. localtunnel.me and loca.lt are two names for the same public server, which both resolve to the same address. The client talks to the first, and visitor URLs end in the second. The server answers with a subdomain, a port, and how many connections to open:
{"id":"every-islands-hide","port":34205,"max_conn_count":2,"url":"https://every-islands-hide.loca.lt"}
The client then opens that many plain TCP connections to localtunnel.me on that port (two, on the day of these runs), and visitor requests reach your machine over them. Each time one of those connections closes, the client opens a new one to the same port. It never repeats that first request, so the port from the first answer is the only one it will ever use. With DEBUG=localtunnel:* set you can watch it happen after a single request:
2026-10-12T07:52:40.751Z localtunnel:client tunnel dead [total: 1]
2026-10-12T07:52:40.751Z localtunnel:client establishing tunnel http://localhost:8765 <> localtunnel.me:16323
On its side, loca.lt closes a tunnel's port once no client has been connected to it for about a minute. I measured that by asking for a tunnel with curl "https://localtunnel.me/?new", waiting, and testing the port with nc -vz localtunnel.me <port>. A port no client had connected to was still open after 52 to 55 seconds and refused connections after 60 to 62 seconds, 4 of 4 runs each. Tunnels whose client had disconnected matched that. One came back after a 30-second gap, and another's port refused connections 70 seconds after its client was killed.
So if your connections are gone for longer than that minute, the server drops the tunnel. When the network comes back, the client dials the old port, the server refuses it, and the client turns that refusal into this error and exits. The client source has both. The message is built in lib/TunnelCluster.js↗ for any refused connection, and bin/lt.js↗ rethrows it.
Is it my firewall?
Probably not. The client appends "(check your firewall settings)" to every refused connection to the tunnel server, whatever refused it. On today's loca.lt that is usually the server, for the reason above.
The Stack Overflow answer that suggests sudo ufw disable↗ changes nothing on a default Ubuntu install. The Ubuntu UFW wiki↗ says "By default UFW is disabled", and once enabled its default is "deny (incoming), allow (outgoing)". A firewall can still cause it on a network that rejects outbound connections to high ports (rather than silently dropping them). Then the error shows up on every start, and the same command works from another network.
How to reproduce the crash
To trigger the error on demand, I loaded a small script into lt with node --require. On a signal it drops the client's connections to the server and holds new ones back for a set time, which is roughly what a closed laptop lid or a dead Wi-Fi does:
// Preload for `lt`: on SIGUSR2, drop every socket to localtunnel.me and hold
// reconnects for SLEEP_MS, the way a closed laptop lid or a dead Wi-Fi does.
const net = require('net')
const SLEEP_MS = Number(process.env.SLEEP_MS || 90000)
const REMOTE = 'localtunnel.me'
const sockets = new Set()
let wakeAt = 0
const connect = net.connect
net.connect = function (opts, ...rest) {
if (typeof opts !== 'object' || opts.host !== REMOTE) return connect.call(this, opts, ...rest)
const wait = wakeAt - Date.now()
let sock
if (wait > 0) {
sock = new net.Socket()
setTimeout(() => sock.connect(opts), wait)
} else {
sock = connect.call(this, opts, ...rest)
}
sockets.add(sock)
sock.once('close', () => sockets.delete(sock))
return sock
}
process.on('SIGUSR2', () => {
wakeAt = Date.now() + SLEEP_MS
console.error(`[sleep-sim] ${new Date().toISOString()} offline for ${SLEEP_MS / 1000}s, dropping ${sockets.size} sockets`)
for (const s of sockets) s.destroy()
setTimeout(() => console.error(`[sleep-sim] ${new Date().toISOString()} back online`), SLEEP_MS)
})
SLEEP_MS=90000 node --require ./sleep-sim.js ./node_modules/.bin/lt --port 8765 --subdomain lc-sleep-16722
kill -USR2 <pid of that node process>
A 90-second gap produced the error at the top of this post. A 30-second gap did not:
| Gap | What visitors got | What lt did |
|---|---|---|
| 30 s | HTTP 200, 5 s after the gap ended | Kept running |
| 90 s | A request 5 s into the gap got a 502 Bad Gateway after 10.5 s, then 503 - Tunnel Unavailable at about 75 s | Exited with the error the moment the network came back |
When it fails right after lt starts
If nothing slept and lt dies within seconds of printing its URL, the port was closed before your client reached it. That usually points to loca.lt, not your machine. In the GitHub issue for this error↗, the maintainer put repeat episodes in July and August 2025 down to downtime, then possible TCP port exhaustion, then an overloaded server. In issue 726↗ from December 2025, the opener reports tunnels that "only stay up for about a minute when first started", and one reply quotes this same error.
Check status.loca.lt↗, the status page the maintainer set up in issue 697↗. To rule out your own network, run the same lt command from another one, such as a phone hotspot. If it fails there too, the problem is on loca.lt's side. Your options are to wait, to switch to another tunnel service (the ngrok alternatives comparison covers free tiers, localtunnel included), or to run your own server from localtunnel's server repository↗, last changed in March 2019, and point lt --host at it.
Restart lt with the same subdomain
The only way to get a working port is a new tunnel request (/?new, or /my-app when you pass --subdomain my-app), which means a new lt process. Pass --subdomain so the new process usually gets the same URL back. The name is not guaranteed (the localtunnel README↗ says you may not receive it, depending on availability), but without the flag every restart gets a new random name. The name also stays taken until loca.lt drops the old tunnel, so a restart within a minute of the old client stopping gets a random one. Once the old URL returns a 503, the name is free again.
lt --port 3000 --subdomain my-app
To survive the next sleep without watching the terminal, run it in a loop:
while true; do lt --port 3000 --subdomain my-app; sleep 2; done
I ran this loop through the same 90-second gap. The client crashed with the error when the network came back, the loop started a new one, it got lc-loop-7221.loca.lt back, and the URL answered 200 10 seconds after the gap. The name came back because by the time lt crashes, loca.lt has already dropped the old tunnel. The 2-second pause keeps the loop from spinning if lt exits straight away.
If you call localtunnel from Node rather than the CLI, the rule is the same, since only a new localtunnel() call gets a new port. The tunnel emits error once per refused connection (twice on today's loca.lt), so keep the listener attached with on and open the replacement only once.
A loop brings the URL back once you are online again, but visitors still hit the 502 and the 503 while you are away. What they see in that window depends on the service. In LocalCan, for example, a Public URL (also called a tunnel) that you add in the app or a project file stays reserved while the app is closed or the computer is off (Public URLs), and visitors get an offline page in the meantime (Snapshots). On the Free plan, though, a URL lasts up to 60 minutes. For a one-off like lt --port 3000, the CLI has localcan http 3000, which publishes a Public URL once a license key is activated, including a Free plan key (quick tunnels).
Verify the tunnel is back
curl -s -o /dev/null -w "%{http_code}\n" -H "bypass-tunnel-reminder: 1" https://my-app.loca.lt
Plain curl gets through without the header, since localtunnel shows its reminder page only to browser User-Agents. The header keeps the check valid from a tool that sends one. A 200 means the tunnel is back. A 503 means no client is connected for that name yet. If curl hangs with no answer, the tunnel is up but nothing is listening on the local port. In my run with the app stopped, two requests got no response within 60 seconds, and the client retried the local port once a second, which it only logs under DEBUG.
Other localtunnel errors
| What you see | What it means | What to do |
|---|---|---|
No output from lt, no URL | The client's first request (to loca.lt, or wherever --host points) is failing, and the client retries it every second without output. Two causes are an exported HOST variable, since lt reads its options from the environment and treats HOST as --host, and an invalid --subdomain, which loca.lt answers with a 403 that the client retries forever | Run with DEBUG=localtunnel:* to see the reason. Run unset HOST, or pass --host https://localtunnel.me. Use a lowercase subdomain of 4 to 63 characters |
A plain 503 - Tunnel Unavailable page | No client is connected for that subdomain. loca.lt returns it for a tunnel it dropped a minute ago and for a name nobody has used | Restart lt. If lt crashed with the error above, this is the same event seen by the visitor |
A Bad Gateway page after about 10 seconds | Your client's connections dropped within the last minute, and the server still holds the port | Get back online within the minute. If lt has exited, wait for the 503, then restart it with the same --subdomain |
| A "Tunnel website ahead!" page asking for an IP (HTTP 511) | The reminder page localtunnel shows browsers once per visitor IP every 7 days. The password is the public IP of the machine running lt, which https://loca.lt/mytunnelpassword prints when opened on that machine | Enter it, or send a bypass-tunnel-reminder header from code. The ngrok alternatives comparison shows the page next to other free tiers |
The HOST case is easy to hit, because some dev servers read the same variable to pick the address they listen on, Create React App among them (Create React App's environment variables↗). In a clean node:20-alpine container, lt --port 3000 printed its URL. After export HOST=0.0.0.0, the same command printed nothing for 8 seconds, DEBUG=localtunnel:* showed tunnel server offline: connect ECONNREFUSED ::1:80, retry 1s once a second, and adding --host https://localtunnel.me brought the URL back.
