A binding gives your code a ready-to-use object on env for one resource. There are no keys or URLs in your code, the resource stays private, and the binding name is yours (env.CACHE, env.DB…). Binding names are UPPER_SNAKE_CASE by convention.
KV is eventually consistent and has no atomic increment: two requests that read, add one and write can lose an update. Use D1 (UPDATE … SET n = n + 1) or a Durable Object for counters.
SQLite with the D1 API. Statements are prepared once and bound with values; never build SQL by joining strings.
js
const stmt = env.DB.prepare("SELECT id, title FROM posts WHERE author = ?1 AND published = ?2");
const { results } = await stmt.bind(authorId, 1).all(); // { results: rows[], success, meta }
const post = await env.DB.prepare("SELECT * FROM posts WHERE id = ?").bind(id).first(); // row | null
const title = await env.DB.prepare("SELECT title FROM posts WHERE id = ?").bind(id).first("title");
const rows = await env.DB.prepare("SELECT id, title FROM posts").raw(); // [[1, "a"], …]
const info = await env.DB
.prepare("INSERT INTO posts (title, author) VALUES (?, ?)")
.bind(title, authorId)
.run();
console.log(info.meta.last_row_id, info.meta.changes);
// several statements in one transaction: all succeed or none
await env.DB.batch([
env.DB.prepare("INSERT INTO orders (id, total) VALUES (?, ?)").bind(orderId, total),
env.DB.prepare("UPDATE stock SET qty = qty - 1 WHERE sku = ?").bind(sku),
]);
// schema scripts (several statements, no results)
await env.DB.exec("CREATE TABLE IF NOT EXISTS tags (name TEXT PRIMARY KEY)");
Placeholders are ? (in order) or ?1, ?2 (by position). Values may be strings, numbers, null, booleans (stored as 0/1) and ArrayBuffer (BLOB).
Reads may come from a nearby replica. To read your own writes within a request flow, use a session: const db = env.DB.withSession("first-primary"), then use db.prepare(…); pass db.getBookmark() to the next request to continue the same session.
Create tables with migrations (rr d1 migrations create / apply) or in the console's query editor; Studio runs schema SQL for you.
// upload
await env.FILES.put("avatars/42.png", request.body, {
httpMetadata: { contentType: "image/png", cacheControl: "public, max-age=86400" },
customMetadata: { uploadedBy: "42" },
});
// download (null when missing)
const obj = await env.FILES.get("avatars/42.png");
if (!obj) return new Response("not found", { status: 404 });
const headers = new Headers();
obj.writeHttpMetadata(headers); // content-type, cache-control, …
headers.set("etag", obj.httpEtag);
return new Response(obj.body, { headers });
// part of a file, or only if changed
const part = await env.FILES.get("video.mp4", { range: { offset: 0, length: 1024 * 1024 } });
const fresh = await env.FILES.get("data.json", { onlyIf: { etagDoesNotMatch: knownEtag } });
// metadata only
const head = await env.FILES.head("avatars/42.png"); // size, etag, uploaded (Date), httpMetadata, customMetadata
// list, a folder at a time
const page = await env.FILES.list({ prefix: "avatars/", delimiter: "/", limit: 100, cursor });
// page.objects[], page.delimitedPrefixes[], page.truncated, page.cursor
// delete one or many
await env.FILES.delete("avatars/42.png");
await env.FILES.delete(["a.txt", "b.txt"]);
An object (get, head, list entries) has key, size, etag, httpEtag, uploaded (a Date), httpMetadata and customMetadata; a get result also has body, text(), json(), arrayBuffer() and blob(). When an onlyIf condition fails, get returns the object with body: null (and null only when the key does not exist). range takes { offset, length } or { suffix }. Files over a few hundred MB: use createMultipartUpload(key) → uploadPart(n, chunk) → complete(parts).
await env.JOBS.send({ type: "welcome-email", userId }); // any JSON-able value
await env.JOBS.send(payload, { delaySeconds: 600 }); // deliver in 10 minutes
await env.JOBS.sendBatch([{ body: a }, { body: b, delaySeconds: 30 }]);
The consuming app exports queue(batch, env, ctx). Each message has id, body, attempts and timestamp, and ack() / retry({ delaySeconds }); the batch has ackAll() and retryAll(). Unacknowledged messages are retried, then go to the dead-letter queue. See Queues.
env.STATS.writeDataPoint({
indexes: [customerId], // one value: groups and samples
blobs: ["checkout", country], // up to 20 strings: blob1…blob20
doubles: [amountCents, items], // up to 20 numbers: double1…double20
});
Fire and forget; it never slows the request. Chart and query on the dataset's page. See Analytics.
Call another of your apps directly, without going over the internet and without making it public:
js
// HTTP style: any Request
const res = await env.AUTH.fetch("https://auth/verify", { headers: { authorization: token } });
// RPC style: call methods of the other app's default export or a named entrypoint
const user = await env.AUTH.verify(token);
// in the auth-api app
import { WorkerEntrypoint } from "cloudflare:workers";
export default class extends WorkerEntrypoint {
async fetch(request) { return new Response("ok"); }
async verify(token) { return { id: "42", plan: "pro" }; }
}
Arguments and results are structured-cloned (plain data, Dates, Maps, ArrayBuffers); functions cannot be passed. entrypoint: "AdminApi" in the binding targets export class AdminApi extends WorkerEntrypoint.
One instance per id, on one node, with its own strongly consistent storage. Use them for anything every visitor must see the same way at the same moment: a room, a counter, a rate limiter, a lock.
js
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject { // this.ctx (state) and this.env are set for you
async increment(by = 1) {
const n = ((await this.ctx.storage.get("n")) ?? 0) + by;
await this.ctx.storage.put("n", n);
return n;
}
}
export default {
async fetch(request, env) {
const id = env.COUNTER.idFromName("global"); // same name → same instance, everywhere
const n = await env.COUNTER.get(id).increment(); // RPC to the instance
return Response.json({ n });
},
};
Storage: get(key | keys[]), put(key, value | entries), delete(key | keys[]), deleteAll(), list({ prefix, start, startAfter, end, limit, reverse }), transaction(async (txn) => …) (all-or-nothing). Keep each value under 128 KB; use SQL storage for larger or relational state.
SQL storage: await this.ctx.storage.sql.exec("SELECT * FROM t WHERE id = ?", id).toArray() (also .one(), .raw()). Unlike Cloudflare, exec is asynchronous here: always await the cursor.
RPC on a stub (env.COUNTER.get(id).increment(2)) takes and returns JSON-serialisable values.
Alarms: await this.ctx.storage.setAlarm(Date.now() + 60_000) and an async alarm() method; getAlarm(), deleteAlarm().
await env.EMAIL.send({
from: "noreply@example.com", // an address on the bound domain
fromName: "Example",
to: "user@gmail.com", // one address; cc and bcc take arrays, replyTo one address
subject: "Welcome",
text: "Thanks for signing up.",
html: "<p>Thanks for signing up.</p>",
});
The domain needs a verified DKIM key under Email routing. Receiving mail is the email() handler.
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) return api(request, env);
return env.ASSETS.fetch(request); // index.html, CSS, images from ./public
},
};
With assets.directory in the config (or a static site app), files are served from the edge; env.ASSETS.fetch() lets your code fall back to them. Single-page apps: enable SPA mode so unknown paths return index.html.