ch08-kv
在 GitHub 上檢視·4 個檔案·4.3 KB
取得並執行
這個範例可以獨立 clone 執行,不依賴其他章節。
npx degit mrsuner/cloudflare-serverless-platform-manuals/examples/ch08-kv
cd ch08-kv
npm install可用指令
npm run dev # wrangler dev
npm run deploy # wrangler deploy
npm run typecheck # tsc --noEmit
npm run cf-typegen # wrangler types --env-interface CloudflareBindings說明
ch08 — Workers KV behaviour probes
Section titled “ch08 — Workers KV behaviour probes”Companion example for docs/08-kv.md.
Every limit in the chapter was hit here on purpose. No Cloudflare account
needed — everything runs against local .wrangler/state/.
npm install && npm run devB=localhost:8787curl -s "$B/seed?n=120"curl -s "$B/bulk?n=100"curl -s "$B/bulk?n=101"curl -s "$B/ttl"curl -s "$B/metadata"curl -s "$B/list"curl -s "$B/cachettl"curl -s "$B/hammer"Verified findings
Section titled “Verified findings”2026-07-28 · wrangler@4.114.0 · workerd@1.20260722.1
Bulk get: exactly 100 keys, one operation
Section titled “Bulk get: exactly 100 keys, one operation”{"requested":100,"ok":{"type":"Map","size":100,"sample":[["k:0000",{"i":0}],["k:0001",{"i":1}]]}}{"requested":101,"threw":"Error: KV GET_BULK failed: 400 You can request a maximum of 100 keys"}100 keys bill as one operation. At $0.50/million reads that is a 100x cost difference, and it saves 99 subrequests against the Free plan’s 50.
expirationTtl floor is 60s
Section titled “expirationTtl floor is 60s”{"ttl=10": {"threw":"KV PUT failed: 400 Invalid expiration_ttl of 10. Expiration TTL must be at least 60."}, "ttl=59": {"threw":"KV PUT failed: 400 Invalid expiration_ttl of 59. Expiration TTL must be at least 60."}, "ttl=60": {}, "ttl=61": {}}Metadata limit counts the serialised length
Section titled “Metadata limit counts the serialised length”{"bytes=100": {}, "bytes=1000": {}, "bytes=1024": {"threw":"KV PUT failed: 413 Metadata length of 1034 exceeds limit of 1024."}, "bytes=2000": {"threw":"KV PUT failed: 413 Metadata length of 2010 exceeds limit of 1024."}}1024 bytes of payload fails — the {"pad":"..."} wrapper adds 10 bytes.
cacheTtl floor is 30s (was 60 before 2026-01-30)
Section titled “cacheTtl floor is 30s (was 60 before 2026-01-30)”{"cacheTtl=10":{"threw":"KV GET failed: 400 Invalid cache_ttl of 10. Cache TTL must be at least 30."}, "cacheTtl=29":{"threw":"... Cache TTL must be at least 30."}, "cacheTtl=30":{"ok":"{\"i\":0}"},"cacheTtl=60":{"ok":"{\"i\":0}"}}list() — a short page is not the last page
Section titled “list() — a short page is not the last page”{"keys":[{"name":"k:0000","metadata":{"even":true}}, ...], "list_complete":false,"cursor":"azowMDAy","cacheStatus":null, "naiveDoneCheck":false}keys.length === limit === 3 yet list_complete is false. KV can also
return a shorter page that still is not the end, because it scans internal
shards. Always branch on list_complete.
Note metadata comes back inside list() results — putting list-view fields
there removes the need for a per-key get().
The generated types model this correctly: only the list_complete: false
branch carries cursor.
The 1 write/sec/key limit is NOT enforced locally
Section titled “The 1 write/sec/key limit is NOT enforced locally”{"writes":8,"results":[{},{},{},{},{},{},{},{}],"final":"7"}Eight consecutive writes to one key, all accepted. In production the excess writes are dropped silently. Local testing gives no signal at all — this has to be caught in review.
原始碼
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ch08-kv",
"main": "src/index.ts",
"compatibility_date": "2026-07-24",
"observability": { "enabled": true },
"kv_namespaces": [{ "binding": "KV", "id": "0000000000000000000000000000cccc" }]
}package.json
{ "name": "ch08-kv", "private": true, "type": "module",
"scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "typecheck": "tsc --noEmit",
"cf-typegen": "wrangler types --env-interface CloudflareBindings" },
"devDependencies": { "typescript": "^5.9.0", "wrangler": "^4.114.0" } }src/index.ts
// ---------------------------------------------------------------------------
// Chapter 08 — Workers KV behaviour probes.
// ---------------------------------------------------------------------------
const t = async (fn: () => Promise<unknown>): Promise<unknown> => {
try { return { ok: await fn() }; }
catch (e) { return { threw: String(e).slice(0, 160) }; }
};
export default {
async fetch(request: Request, env: CloudflareBindings): Promise<Response> {
const url = new URL(request.url);
const kv = env.KV;
switch (url.pathname) {
case "/seed": {
const n = Number(url.searchParams.get("n") ?? 5);
for (let i = 0; i < n; i++) {
await kv.put(`k:${String(i).padStart(4, "0")}`, JSON.stringify({ i }), {
metadata: { even: i % 2 === 0 },
});
}
return Response.json({ seeded: n });
}
// Bulk get: array key, returns a Map. Max 100 keys.
case "/bulk": {
const n = Number(url.searchParams.get("n") ?? 5);
const keys = Array.from({ length: n }, (_, i) => `k:${String(i).padStart(4, "0")}`);
const r = await t(async () => {
const m = await kv.get(keys, "json");
return { type: m?.constructor?.name, size: m?.size, sample: [...(m ?? [])].slice(0, 2) };
});
return Response.json({ requested: n, ...(r as object) });
}
// expirationTtl has a documented 60s floor.
case "/ttl": {
const out: Record<string, unknown> = {};
for (const ttl of [10, 59, 60, 61]) {
out[`ttl=${ttl}`] = await t(() => kv.put(`ttl:${ttl}`, "v", { expirationTtl: ttl }));
}
return Response.json(out);
}
// metadata is capped at 1024 bytes serialised.
case "/metadata": {
const out: Record<string, unknown> = {};
for (const size of [100, 1000, 1024, 2000]) {
out[`bytes=${size}`] = await t(() =>
kv.put(`meta:${size}`, "v", { metadata: { pad: "x".repeat(size) } }),
);
}
return Response.json(out);
}
case "/list": {
const page = await kv.list<{ even: boolean }>({ prefix: "k:", limit: 3 });
return Response.json({
keys: page.keys,
list_complete: page.list_complete,
cursor: "cursor" in page ? page.cursor : null,
cacheStatus: page.cacheStatus,
// The trap: keys.length < limit does NOT mean you are done.
naiveDoneCheck: page.keys.length < 3,
});
}
// A missing key is null, and the lookup is still billed.
case "/miss":
return Response.json({ value: await kv.get("definitely-not-here") });
// Hammering a single key: the 1 write/sec/key limit is a production
// behaviour, not enforced locally.
case "/hammer": {
const results: unknown[] = [];
for (let i = 0; i < 8; i++) results.push(await t(() => kv.put("hot", String(i))));
return Response.json({ writes: results.length, results, final: await kv.get("hot") });
}
case "/cachettl": {
const out: Record<string, unknown> = {};
for (const c of [10, 29, 30, 60]) {
out[`cacheTtl=${c}`] = await t(() => kv.get("k:0000", { cacheTtl: c }));
}
return Response.json(out);
}
default:
return new Response(
"try /seed?n=120 /bulk?n=100 /bulk?n=101 /ttl /metadata /list /miss /hammer /cachettl\n",
{ status: 404 },
);
}
},
} satisfies ExportedHandler<CloudflareBindings>;tsconfig.json
{ "compilerOptions": { "target": "esnext", "lib": ["esnext"], "module": "esnext",
"moduleResolution": "bundler", "types": ["./worker-configuration.d.ts"],
"strict": true, "skipLibCheck": true, "noEmit": true, "isolatedModules": true },
"include": ["src/**/*.ts", "worker-configuration.d.ts"] }