Wrangler CLI 2026 - Cloudflare Workers Deep Dive for Indian Devs
Ship edge functions from India with Wrangler CLI 2026. Setup, deploy, R2, D1, and cost breakdowns in INR.

Why Wrangler matters in 2026
Wrangler is the command line tool you use to create, run, configure, and deploy Cloudflare Workers. If you are building APIs for Indian users, the main reason to care is simple: you can move request handling closer to the user without managing servers in Mumbai, Singapore, Frankfurt, and Virginia yourself.
Do not treat edge as magic latency dust. Measure your own routes from Airtel, Jio, office broadband, and mobile networks before promising numbers. A Worker served from a nearby Cloudflare location can be fast from India, but exact latency depends on peering, route, cache state, and whether your Worker calls D1, R2, KV, an origin API, or another service behind the scenes.
As of this polish pass on 2026-05-20, npm reports [email protected]. Cloudflare recommends installing Wrangler locally in each project, not as a global binary. That matters because one repo can pin Wrangler 4.93.0 while another stays on a known-good older 4.x release until you are ready to test an upgrade.
Prerequisites
Use a current Node.js release supported by Cloudflare's tooling. Node.js 22 LTS is a sensible baseline in 2026, and Node.js 24 Current is fine if your team has already standardized on it.
Create a project and install Wrangler as a dev dependency:
npm create cloudflare@latest -- my-api --type=hello-world --ts
cd my-api
npm install --save-dev [email protected]
npx wrangler --version
Authenticate once on your machine:
npx wrangler login
This opens a browser OAuth flow. For CI, do not use browser login. Create an API token with the minimum permissions required for Workers, D1, and R2, then inject it as an environment secret in your CI provider. Before you script anything against that token, read the Cloudflare API token gotchas: the self-update endpoint is a full replace that silently drops scopes, and the wrangler auth error it throws explains nothing.
Project shape
A small TypeScript Worker usually starts with this structure:
my-api/
src/
index.ts
wrangler.toml
package.json
tsconfig.json
The wrangler.toml file is the contract between your code and Cloudflare's runtime:
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-05-20"
The compatibility_date is not decoration. It pins runtime behavior so Cloudflare can ship platform changes without silently changing how your Worker behaves. Review it during planned upgrades, bump it intentionally, and run your test suite before deploying.
Local development
Run the local dev server:
npx wrangler dev
Wrangler starts a local Workers runtime and exposes your Worker on http://localhost:8787 by default. Keep one detail in mind: local development is close to production behavior, but it is not proof that every remote integration is correct. Use local mode while building, then test remote bindings before a serious deploy.
Replace src/index.ts with a minimal API:
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json({ status: "ok", region: "local" });
}
return new Response("Hello from Cloudflare Workers", { status: 200 });
},
};
Test it:
curl http://localhost:8787/health
Expected response:
{"status":"ok","region":"local"}
Add D1 for SQLite-backed data
D1 is Cloudflare's serverless SQL database built around SQLite. It is a good fit for compact relational data, internal dashboards, API metadata, and read-heavy product tables. It is not a drop-in replacement for a tuned Postgres cluster with long transactions, large analytical queries, or high write contention.
Create a database:
npx wrangler d1 create my-api-db
Wrangler prints the binding block. Add it to wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-api-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Create schema.sql:
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
Apply the schema locally:
npx wrangler d1 execute my-api-db --local --file=./schema.sql
Use the binding in TypeScript:
interface Env {
DB: D1Database;
}
type CreateUserBody = {
email: string;
};
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/users" && request.method === "POST") {
const body = (await request.json()) as CreateUserBody;
if (!body.email || !body.email.includes("@")) {
return Response.json({ error: "invalid email" }, { status: 400 });
}
await env.DB.prepare("INSERT INTO users (email) VALUES (?)")
.bind(body.email)
.run();
return Response.json({ created: true }, { status: 201 });
}
if (url.pathname === "/users" && request.method === "GET") {
const { results } = await env.DB.prepare(
"SELECT id, email, created_at FROM users ORDER BY id DESC LIMIT 50",
).all();
return Response.json(results);
}
return new Response("Not found", { status: 404 });
},
};
Test the flow:
npx wrangler dev
curl -X POST http://localhost:8787/users \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}'
curl http://localhost:8787/users
For production, apply migrations to the remote database before deploying code that depends on them:
npx wrangler d1 execute my-api-db --remote --file=./schema.sql
Add R2 for object storage
R2 is Cloudflare's object storage service. The pricing reason to care is egress: Cloudflare's Workers pricing page lists no R2 egress bandwidth charge. You still pay for storage and request classes after the included allowances, so small hot objects can cost more through operations than through bytes stored.
Create a bucket:
npx wrangler r2 bucket create my-api-assets
Add the binding:
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "my-api-assets"
Use the bucket from the Worker:
interface Env {
ASSETS: R2Bucket;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/upload" && request.method === "PUT") {
const key = url.searchParams.get("key");
if (!key) {
return Response.json({ error: "missing key" }, { status: 400 });
}
await env.ASSETS.put(key, request.body, {
httpMetadata: {
contentType: request.headers.get("content-type") ?? "application/octet-stream",
},
});
return Response.json({ uploaded: key });
}
if (url.pathname.startsWith("/files/") && request.method === "GET") {
const key = decodeURIComponent(url.pathname.replace("/files/", ""));
const object = await env.ASSETS.get(key);
if (!object) {
return new Response("Not found", { status: 404 });
}
return new Response(object.body, {
headers: {
"Content-Type": object.httpMetadata?.contentType ?? "application/octet-stream",
},
});
}
return new Response("Not found", { status: 404 });
},
};
Test it locally:
printf "Hello R2\n" > test.txt
curl -X PUT "http://localhost:8787/upload?key=test.txt" \
-H "Content-Type: text/plain" \
--data-binary @test.txt
curl http://localhost:8787/files/test.txt
Deploying
Deploy the Worker:
npx wrangler deploy
Wrangler will print the deployed route, usually a workers.dev URL unless you have configured a custom domain. For a production Indian SaaS, configure a real domain early. It makes cookies, auth redirects, API clients, and monitoring cleaner than shipping public beta traffic on a temporary subdomain.
Use environments for staging and production:
[env.staging]
name = "my-api-staging"
[[env.staging.d1_databases]]
binding = "DB"
database_name = "my-api-db-staging"
database_id = "staging-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[env.staging.r2_buckets]]
binding = "ASSETS"
bucket_name = "my-api-assets-staging"
[env.production]
name = "my-api-production"
[[env.production.d1_databases]]
binding = "DB"
database_name = "my-api-db"
database_id = "prod-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[env.production.r2_buckets]]
binding = "ASSETS"
bucket_name = "my-api-assets"
Deploy each environment explicitly:
npx wrangler deploy --env staging
npx wrangler deploy --env production
If these deploys run from GitHub Actions and you watch your free minutes drain across repos, move the job to a self-hosted GitHub Actions runner on Oracle ARM and the CI cost drops to zero.
Cost notes for Indian devs
Use USD as the billing source of truth and convert to INR only for budgeting. Exchange rates, card markup, GST treatment, and invoice handling can change. At 1 USD = 83.5 INR, the Workers Paid plan minimum of 5 USD is about 418 INR per month.
The important current limits are these:
| Item | Free plan | Paid plan |
|---|---|---|
| Worker requests | 100,000 per day | 10 million per month included, then usage billing |
| Worker CPU | 10 ms per invocation | 30 million CPU ms per month included |
| D1 rows read | 5 million per day | 25 billion per month included |
| D1 rows written | 100,000 per day | 50 million per month included |
| D1 storage | 5 GB total | 5 GB included, then per-GB billing |
| R2 storage | 10 GB-month included | per-GB storage after included usage |
| R2 egress | no egress bandwidth charge | no egress bandwidth charge |
Do not compare Workers to AWS Lambda only on request count. A real comparison includes database reads, object operations, logs, CPU time, static asset behavior, and whether your origin is still in another cloud. For small CRUD APIs, Workers plus D1 plus R2 can stay cheap. For CPU-heavy image processing, large dependency bundles, or long background jobs, the fit is weaker.
Production checklist
Before sending real traffic:
- Pin
wranglerindevDependencies, and update it through pull requests. - Keep
compatibility_datecurrent, but bump it intentionally. - Use
npx wrangler secret put JWT_SECRETfor secrets. - Run
npx wrangler tailduring launch to catch runtime errors. - Check compressed Worker size with
npx wrangler deploy --dry-run --outdir bundled. - Remember that compressed Worker size limits differ by plan: 3 MB on Free and 10 MB on Paid.
- Move large static files, model files, and generated assets to R2 or Workers Static Assets.
- Add indexes to D1 tables before traffic teaches you what full scans cost.
Quick takeaways
- Use local
[email protected], not an unpinned global install. - Start with
npm create cloudflare@latest, then usenpx wrangler devandnpx wrangler deploy. - D1 is strong for compact SQLite-backed app data, but you still need indexes and migration discipline.
- R2 removes egress bandwidth charges, while storage and operation charges still matter.
- Treat INR costs and India latency numbers as measurements to verify, not constants to copy.
Related
More AI Coding

Building a Custom MCP Server in Python: Claude Reaches My Stack
Claude Code is sharp until it hits the edge of your machine and your private tools. I wrote three small MCP servers in Python to close that gap. Here is the real pattern, the real gotcha that bit me, and what it costs.

Claude Code Subagents in Practice: Fork Flag, Cache Leak, Worktree Trap
Fanning out subagents in Claude Code looks free until you hit the cap or your forks clobber each other's commits. These are the real fixes I learned running fanouts: the fork env flag that shares the parent's cache, the WebFetch cache leak, and the worktree pattern for parallel writers.

I Gave My AI Agents a Memory With SQLite FTS5 (No Vector DB)
Most agent-memory setups reach for Pinecone or pgvector by reflex. I put 2000+ markdown files behind SQLite FTS5 with BM25 ranking, and my agents now answer their own 'who is X' questions in under a second for zero tokens. Here is the schema, the query, and the one place lexical search loses.