Bypassing ISP DNS Blocks with Cloudflare Workers

A complete, copy-paste guide to keep your app's API reachable when an ISP (e.g. Jio) blocks your hosting provider's DNS — by fronting it with your own domain on Cloudflare.

Worked example: a backend API on Railway, consumed by mobile & web apps  •  Cloudflare free tier  •  ~15 minutes

What this solves

Your backend works perfectly on WiFi and broadband but fails on a specific mobile network (in this guide: Jio in India). The app shows connection/timeout errors only on that network.

The cause is almost never your code. The ISP is blocking DNS resolution of your hosting provider's domain — for example anything ending in .up.railway.app. The phone literally cannot resolve the server's address, so the request never leaves the device.

The core idea: Put a domain you own in front of the API, served from Cloudflare's global edge. ISPs don't block Cloudflare. A tiny Cloudflare Worker receives each request and forwards it to your real origin server from Cloudflare's network — so the user's device only ever resolves Cloudflare, never the blocked host.

How the traffic flows

📱 Phone on Jio │ resolves api.example.com ▼ ☁️ Cloudflare edge (NOT blocked by Jio) │ Worker runs here, calls fetch() ▼ 🚂 my-backend.up.railway.app (resolved by Cloudflare, server-side) │ ▼ Response travels back the same path

Because the fetch() to the origin happens inside Cloudflare's network, Jio's DNS block never applies — Cloudflare resolves the origin itself. The client only ever talks to api.example.com.

No CORS changes needed. Browser CORS keys off the Origin header (your web app's domain), which does not change when you swap the API host. Proxying the API is invisible to CORS.

Prerequisites

1Add your domain to Cloudflare

  1. Log in to the Cloudflare dashboard and click Add a site.
  2. Enter your domain (example.com) and pick the Free plan.
  3. Cloudflare scans your existing DNS records. Review them.
  4. Cloudflare gives you two nameservers. Go to your domain registrar (where you bought the domain) and replace its nameservers with Cloudflare's.
  5. Wait for activation (minutes to a few hours). When the dashboard shows DNS Setup: Full / "Active", you're ready.
If your domain is already on Cloudflare (the DNS records page shows your existing records, all "Proxied"), you can skip this step entirely.

2Create the Cloudflare Worker

  1. In the dashboard sidebar, open Workers & Pages.
  2. Click CreateCreate Worker (or "Start with Hello World!").
  3. Give it a name, e.g. api-proxy, and click Deploy.
  4. Click Edit code (top right). Delete the sample code and paste the reverse-proxy script below.
  5. Click Deploy again to save it.

The Worker code

Change the ORIGIN constant to your real server. Everything else is generic.

// Reverse-proxy your-domain -> origin server, server-side from Cloudflare's edge.
// The client only ever resolves Cloudflare; the origin host is never exposed.
const ORIGIN = "https://my-backend.up.railway.app";

export default {
  async fetch(request) {
    const url = new URL(request.url);

    // 1:1 path passthrough. If your client base is
    //   https://api.example.com/api
    // incoming paths already start with /api, which the origin also expects.
    const target = ORIGIN + url.pathname + url.search;

    // Copy headers but drop the incoming Host so fetch() sets the
    // correct Host for the origin (otherwise the origin won't recognise
    // the request and routing breaks).
    const headers = new Headers(request.headers);
    headers.delete("host");

    const init = {
      method: request.method,
      headers,
      redirect: "manual",
    };

    // Forward the body for non-GET/HEAD methods (POST, PUT, PATCH, DELETE).
    if (request.method !== "GET" && request.method !== "HEAD") {
      init.body = request.body;
    }

    return fetch(target, init);
  },
};
Why headers.delete("host") matters: If you forward the incoming Host: api.example.com to the origin, it won't recognise that host and will fail to route. Deleting it lets fetch() set Host to the origin's own hostname automatically.

3Connect your subdomain (Custom Domain)

This is the step that makes api.example.com point at the Worker — and it auto-creates the DNS record for you.

  1. Open your Worker → Domains tab (top row, between "Observability" and "Settings").
  2. Click + Add Domain.
  3. A "Connect domain" dialog appears. This box searches for the parent zone, not the full hostname. Select your root domain (example.com) from the list.
    Gotcha: Typing the full api.example.com here shows "No zones found" — because it's matching zone names. Pick the parent example.com first.
  4. On the next step, enter the full hostname: api.example.com, and confirm.
  5. Cloudflare provisions the domain + a free SSL certificate and creates a proxied DNS record automatically. After a minute or two it shows under Custom Domain → Production.
Don't use the free *.workers.dev URL (e.g. api-proxy.you.workers.dev) for production. ISPs and corporate filters sometimes block workers.dev too. Always use your own custom domain.

4Verify it works

From any machine, hit a route through the new domain and check two things: that it resolves to a Cloudflare IP, and that your real API responds.

curl -s -o /dev/null -w "status=%{http_code} server=%{remote_ip}\n" \
  https://api.example.com/api/health

curl -s https://api.example.com/api/health

Example result from a working setup:

status=404 server=104.21.4.221
{"message":"Cannot GET /api/health","error":"Not Found","statusCode":404}
This is success, even though it's a 404. The server IP 104.21.x.x is Cloudflare (not the origin), proving the request went through the edge. And the JSON body is your framework's own 404 format — proving the Worker reached your real API and got a genuine response. /api/health just isn't a real route. Test a route that exists for a 200.

5Point your apps at the new URL

New base URL: https://api.example.com/api. Replace the old origin everywhere your clients reference it. Typical spots in a multi-app codebase:

File (example)What changed
apps/mobile-app/lib/core/api_client.dartMobile default base URL
apps/pro-app/lib/core/api_client.dartMobile default base URL
apps/mobile-app/vercel.jsonMobile web build define
apps/pro-app/vercel.jsonMobile web build define
apps/admin-web/src/lib/api.tsWeb production fallback URL
apps/customer-web/src/lib/api.tsWeb production fallback URL
.vscode/launch.json (+ per-app)Local-run launch defines

Example: mobile (Dart / Flutter)

// before
const String _defaultBaseUrl = 'https://my-backend.up.railway.app/api';
// after
const String _defaultBaseUrl = 'https://api.example.com/api';

Example: web (Next.js)

export const API_BASE =
  process.env.NEXT_PUBLIC_API_BASE_URL ??
  (typeof window !== 'undefined' && window.location.hostname === 'localhost'
    ? 'http://localhost:3001/api'
    : 'https://api.example.com/api');   // <- was the railway URL
Tip: Change the hard-coded default/fallback URL, not just an env var — so a fresh build on the blocked network works without anyone remembering to pass a flag.

Don't forget: server-generated links

The proxy fixes API calls. But anything your server generates that embeds the old origin host will still point at the blocked address when opened on the ISP. Audit these:

Repoint those at api.example.com (or your web domain) as appropriate.

Deploying the changes

Troubleshooting

SymptomCause & fix
Worker returns 1101 / "Worker threw exception" Usually a bad ORIGIN or forwarding the Host header. Confirm headers.delete("host") is present and ORIGIN has no trailing slash.
Response is "Hello World", not your API The Worker still has the sample code. Re-open Edit code, paste the proxy script, and Deploy.
"No zones found" when adding the domain You typed the full subdomain in the zone picker. Select the parent domain first, then enter the subdomain on the next screen.
SSL / certificate error on first calls Cloudflare is still provisioning the cert. Wait a few minutes after adding the custom domain.
Still blocked on the ISP You're probably still hitting the old workers.dev URL or an un-rebuilt app. Confirm the client calls api.example.com and that the DNS record is Proxied (orange cloud).
Large file uploads fail Cloudflare free tier caps request bodies at 100 MB. For larger payloads, upload directly to object storage with a presigned URL instead of through the Worker.

FAQ

Is this against Jio's / the ISP's terms?

No. You're not circumventing any security control — you're serving your own API from your own domain on a mainstream CDN (Cloudflare). This is standard production architecture; most large apps sit behind a CDN anyway.

Does it cost anything?

Cloudflare's free tier includes 100,000 Worker requests/day and free SSL. For many small-to-mid apps that's plenty. Beyond that, the paid Workers plan is inexpensive.

Will it slow my API down?

Negligibly. The Worker runs at the edge nearest the user and adds a few milliseconds. In many cases Cloudflare's network routing to the origin is faster than the user's ISP path.

Do I still need the origin host secret?

The origin URL is inside the Worker, not shipped to clients — so the client never sees or resolves it. That's the whole point and also a nice side benefit (your hosting provider is hidden).

Can I proxy more than one service?

Yes. Add more Workers (one per subdomain) or branch on url.pathname inside a single Worker to route /api, /uploads, etc. to different origins.


Generic worked example: a backend API on Railway, consumed by mobile and web apps, fronted by a Cloudflare Worker at api.example.com. Substitute your own domain and origin throughout. CORS required no change because it keys off the browser Origin, not the API host.