What does this solve?
A small service - a webhook receiver, a redirector, a proxy to someone else's API, a form handler, a scheduled task - requires a disproportionate amount of attention if it lives on a separate machine. System updates, certificate renewal, process monitoring, backups, billing for an idle server. Cloudflare Workers run code on the Cloudflare network without a machine to maintain: deployment is reduced to one command, TLS and global distribution are already enabled, downtime costs nothing.
The second thing that is removed is the cold start delay. The code runs in a V8 isolate, not in a container, so there is no environment startup between the arrival of the request and the first line of the handler. For a webhook that the payment gateway retraces due to a timeout, this is the difference between “accepted” and “retry”.
How it works
Isolates instead of containers
The code runs inside the V8 isolate, the same mechanism that the browser uses to separate tabs. The process is general, isolation is at the engine level. From here the main thing follows: it’s not Node.js underfoot. Web standard set available - fetch, Request, Response, URL, Headers, crypto.subtle, TextEncoder, threads. Compatibility flag nodejs_compat opens a subset of built-in Node modules (node:buffer, node:crypto and others), but a subset, and not the entire runtime.
Entry point
Worker exports an object with handlers:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === '/hook') return handleHook(request, env, ctx);
return new Response('not found', { status: 404 });
},
async scheduled(event, env, ctx) {
await refreshSnapshot(env);
},
async queue(batch, env, ctx) {
for (const msg of batch.messages) await process(msg.body, env);
},
};
env —an object with bindings and secrets. ctx.waitUntil(promise) allows you to respond to the client immediately, and finish the work later: writing a log, sending a notification, warming up the cache.
Config and compatibility date
Settings are in wrangler.jsonc (or wrangler.toml):
{
"name": "hook-receiver",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"kv_namespaces": [{ "binding": "CACHE", "id": "..." }],
"r2_buckets": [{ "binding": "FILES", "bucket_name": "uploads" }],
"triggers": { "crons": ["0 * * * *"] }
}
compatibility_date records the runtime behavior for a specific slice. As long as it doesn't change, platform updates don't change the semantics under the running code. The date is set to the one on which the service was tested, and is moved consciously, as a separate deployment.
Bindings instead of connection strings
The storage facility is coming to env with a ready-made object, and not a URL with a login and password:
- KV —key-value, read-optimized, delayed consistency. Suitable for flags, caches, directories that change rarely.
- D1 —SQL storage on the SQLite engine, with transactions and prepared queries.
- R2 - object storage with S3-compatible API and no outgoing traffic fees. Files, uploads, backups.
- Durable Objects - A named single instance with strong consistency, its own storage, alarms and WebSocket. Coordination point: counter, room, queue for a specific resource.
- Queues —asynchronous processing: received a webhook, posted a message, responded, parsed it later.
- Service bindings —calling another Worker directly, without going to the public network.
- Secrets —
wrangler secret put NAME, nextenv.NAME. They are not in the repository.
The fact that the binding comes as an object eliminates a whole class of leaks: there is nothing to accidentally register and nothing to forget in an environment variable on someone else’s machine.
Statics: Pages and assets in Worker
Pages connects to the git repository, assembles the project on its side, distributes the result and makes a separate preview address for each branch. Dynamics is added by files in the directory functions/, where the file path becomes a route.
In parallel, Workers now have static distribution directly from the worker: static files are declared in the config, everything that is not found among them goes into fetch. Cloudflare reduces both products to Workers, so it’s easier to start a new project right away with the second option - one deployment, one config, one log model.
Local development
wrangler dev raises real runtime locally and emulates disk storage. Flag --remote switches to real bindings in the cloud - this is the only way to see real KV behavior with its propagation delay.
Deploy, versions, logs
wrangler deploy is rolling out a new version. The platform stores previous: wrangler deployments list shows history, wrangler rollback returns the previous one. A gradual rollout allows you to give part of the traffic to the new version and look at errors before switching completely. wrangler tail shows the flow of sales logs in real time.
Useful scripts
- Webhook receiver: check signature, put in queue, respond 200 in milliseconds, parse asynchronously.
- Proxy to an external API: the key lives in the worker’s secret, does not get into the browser, at the same time limits and cache are added.
- Redirects, merging of old URLs and distribution by language or country - at the network border, without changes in the main application.
- Scheduled task: pull the API once an hour, add the snapshot to R2, send a notification if rejected.
- Static site plus two or three endpoints: feedback form, subscription, short links.
- Caching layer in front of the slow origin: the response is placed in the KV or Cache API, the heavy backend receives many times fewer requests.
- Bot on a webhook: receiving an update, responding via the messenger API, dialog state in Durable Object.
Limitations
CPU time per request is limited and the ceiling varies between plans. Heavy image processing, generation of large PDFs, local inference models do not fit into this framework - such tasks are transferred to a separate service or in a queue with another executor.
Memory and bundle size are also limited. An application with a hundred dependencies will not be built there, and “let’s move the entire monolith” is not the right scenario.
Ordinary database drivers that require a long TCP connection do not work directly. For PostgreSQL and MySQL uses Hyperdrive or a driver running over HTTP. This is a separate setting, and not “changed the connection string”.
No file system, no local state between requests. A global variable can survive several requests, or it can disappear at any time - the isolate is recreated without warning. Everything that should live longer than the request goes into the binding.
KV is negotiated lazily: immediately after a write, a read may return the old value. Counters, balances, and balances are not calculated via KV - for this purpose Durable Objects or D1.
There are no long-lived background processes. The demon that hangs and listens cannot be launched; work is cut into handlers based on request, crown and queue messages.
Some of the npm packages are designed for Node and are not compiled into a bundle. Compatibility must be checked before the architecture is supported by the package.
The connection to the platform is real. Durable Objects, KV and Queues have no portable counterpart, and moving means rewriting the storage layer rather than changing hosting.
Debugging is poorer than usual: no ssh, no profiler on the machine. What remains are logs, traces and metrics. An error in recursion will manifest itself not as freezing, but as counting.
How to check the result
Local: wrangler dev, then curl to a local address with the same headers and body that the real sender sends. For webhooks - with a real signature, otherwise the check does not cover the main code branch.
Vs real bindings: wrangler dev --remote. This is where discrepancies between local KV emulation and its real behavior are caught.
Open after deployment wrangler tail and knock on the sales address. A request without a line in the log means that it did not reach the worker: wrong route, wrong domain, DNS record is not proxied. This is the most common reason “the code was rolled out, but nothing changed.”
Check the config with what you tested: compatibility_date and the list of flags in the deployed version must match the local ones. wrangler secret list shows what secrets actually exist in the environment - the missing secret gives undefined, not an error.
The contents of storages are checked by commands wrangler kv key get, wrangler r2 object get, request to D1. If after calling the service the key does not appear, the problem is not reading, but writing.
Under load, look at the processor time per request in observability - the distribution is more important than the average: the ceiling hits the tail, not the median.
Rollback is checked in advance: wrangler deployments list, then wrangler rollback at the stand. The command executed for the first time at the time of the accident is executed slowly.
One line rule: everything that should survive a separate request is stored in the binding (KV, D1, R2, Durable Object), and the global variable in the Worker is only a cache, the loss of which does not break anything.
