// blog

← all posts

Killing the ngrok URL-rotation problem on the free plan

·5 min read·product · free-plan · webhooks

The single most common complaint about ngrok's free plan is the URL rotation: every time you restart the agent, your public URL changes. Stripe, GitHub, Auth0, Discord — every webhook integration breaks until you re-paste the new URL into their dashboard.

ngrok's answer is "upgrade to Pro for static domains, $20/mo." That's a reasonable price point — for a freelancer doing this every day. For a hobbyist, a student, an OSS contributor, or a dev exploring a new framework on the weekend, $20/mo for one feature is hard to swallow.

lrok's answer: free plan includes one reserved subdomain that lives forever. You set the integration URL once, and it stays put.

The implementation

Reservations live in SQLite. One row per claim:

CREATE TABLE reservations (
  user_id TEXT NOT NULL,
  subdomain TEXT NOT NULL,
  description TEXT,
  created_at INTEGER NOT NULL,
  PRIMARY KEY (subdomain),
  FOREIGN KEY (user_id) REFERENCES users(clerk_id)
);

When the agent registers a tunnel with --hint mysite, the tunnel server checks: does mysite exist in reservations? If yes and the user_id matches, allow. If yes and the user_id is someone else's, reject. If no row, treat the hint as ephemeral (first-come).

That's 200 lines of Go including the API endpoints to create / list / delete reservations from the dashboard.

Why it can be free

Reserved subdomains cost nothing on our side. They're TEXT in a SQLite database. The cost ngrok charges for them is positioning — "Pro tier benefit" — not infrastructure cost.

The actual costs of running a tunnel service are: bandwidth, CPU, support. None scales with reservations; they scale with active traffic. Charging $9/mo flat for Pro lets us cover those without needing to nickle-and-dime the free tier on a feature that costs us $0.

Implementation gotchas worth mentioning

Subdomain squatting. You don't want one user grabbing every word in the dictionary. We cap free-plan reservations at 1 (Pro is unlimited). For legitimate dev needs, 1 is plenty. For squatting, 1-per-account makes the economics not work.

Releasing. Users sometimes reserve a name, abandon the project, and never release it. Two policies that worked: a description field surfaced on the dashboard so the user remembers what the name is for, and a "release" button right next to it. We don't auto-expire reservations — it'd break dormant integrations that the user still cares about.

Conflict with running tunnels. If user A has a reservation for mysite and user B (somehow) has an active tunnel using the same subdomain, the active tunnel keeps running. New tunnels can't claim the name. This means deleting a reservation doesn't kick off active sessions — they keep going until the agent disconnects. Saner default; matches the "release the name" verb.

What you can do with one free subdomain

A lot. For most devs, one Stripe + GitHub + Auth0 webhook URL all live behind the same tunnel:

$ lrok reserve mysite
$ lrok http 3000 --hint mysite

Then in the dashboards:

  • Stripe: https://mysite.lrok.io/webhooks/stripe
  • GitHub: https://mysite.lrok.io/webhooks/github
  • Auth0 callback: https://mysite.lrok.io/auth/callback

One tunnel, three integrations, all routed by your local app's path matching.

Try it

$ curl -fsSL https://lrok.io/install.sh | sh
$ lrok login
$ lrok reserve mysite
$ lrok http 3000 --hint mysite

The walkthroughs for Stripe webhooks, GitHub webhooks, and the broader integration list all assume you've reserved a name first.

// try it yourself

lrok is a hosted reverse-tunnel: one command, public HTTPS URL, reserved subdomain on the free plan. $9/mo flat for unlimited.