TL;DR
Cloudflare KV là eventually-consistent key-value store. Write đi tới central store, propagate ra 300+ PoP trong 60 giây. Read đọc từ cache edge PoP gần nhất, rẻ và nhanh (< 10ms).
Luận điểm chính:
KV là cache toàn cầu, không phải database. Thiết kế mọi tải KV giả định đọc nhiều, ghi ít, stale OK ≤ 60 giây. Nếu cần strong consistency hoặc ghi nhanh, không phải tải KV.
Bài này đi qua: consistency model thực tế (có diagram), giới hạn quan trọng nhất, 5 pattern đúng (tính năng cờ, redirect map, session ngắn hạn, asset metadata, cấu hình giới hạn tốc độ), 3 anti-pattern (counter, session chính, primary DB), và gotcha từ blog này.
Dành cho ai
- Dev đã biết cơ bản Workers + binding, chuẩn bị dùng KV lần đầu.
- Người đang dùng KV sai (counter, primary DB) và gặp throttle hoặc stale.
- Ai đang chọn giữa KV / D1 / Durable Object cho tải cụ thể.
Nên đọc trước: Part 3 (3-binding mental model) để biết KV nằm ở đâu trong Storage layer.
Sau bài này bạn sẽ:
- Hiểu consistency model KV và vì sao tốc độ ghi giới hạn ở 1/key/giây.
- Biết 5 pattern KV sáng nhất.
- Tránh 3 anti-pattern biến KV thành cái bẫy.
- Dùng metadata + list() hiệu quả.
Bài này không nói về gì
- Cache API: primitive khác, chạy ở HTTP layer, có bài riêng.
- D1: Part 6.
- Durable Objects: Part 8.
- Workers KV pricing: Part 19.
KV là gì thực sự
KV store đơn giản: put(key, value), get(key), delete(key), list({ prefix }). Key là chuỗi, value là chuỗi hoặc ArrayBuffer hoặc ReadableStream.
Đằng sau API đơn giản đó là kiến trúc central store + edge cache:
- Central store: nguồn chân lý duy nhất. Tất cả ghi đi về đây.
- Edge cache: mỗi PoP có cache riêng. Đọc hit cache → không đi về central.
- Lan truyền: khi ghi, central đẩy cập nhật ra các PoP. Mất vài giây đến 60s tùy PoP và traffic.
Binding
{
"kv_namespaces": [
{ "binding": "CACHE", "id": "abc123..." },
{ "binding": "FLAGS", "id": "def456..." }
]
}
Trong Worker:
async fetch(request, env) {
// get
const cached = await env.CACHE.get("user:123");
// put với TTL
await env.CACHE.put("user:123", JSON.stringify(user), {
expirationTtl: 3600, // 1 giờ
});
// put với metadata (không tính vào value size)
await env.CACHE.put("post:abc", body, {
metadata: { author: "khavan", tags: ["cloudflare"] },
});
// list theo prefix
const result = await env.CACHE.list({ prefix: "user:" });
for (const key of result.keys) {
console.log(key.name, key.metadata);
}
// delete
await env.CACHE.delete("user:123");
}
Consistency model: quan trọng nhất
Đường ghi
Worker (SIN PoP) → put("user:123", v)
→ POST tới central store
→ central persist + broadcast
→ lan truyền ra 300+ PoP trong ≤ 60s
Ghi không xác nhận khi tất cả PoP đã nhận. put() trả về ngay khi central ghi xong (thường < 100ms).
Đường đọc
Worker (LAX PoP) → get("user:123")
→ kiểm tra cache edge local
→ hit: trả ngay (< 10ms)
→ miss: lấy từ central (~50-100ms cross-region)
Tỉ lệ hit đọc thường > 90% trong production. Nhưng cache staleness là vấn đề: PoP LAX có thể cache bản cũ từ 59s trước, trong khi PoP SIN đã có bản mới.
Hệ quả
- Write-your-read cùng PoP: thường thấy ngay (cache local vô hiệu hóa sau ghi).
- Write-then-read cross-PoP: có thể stale tới 60s.
- Cùng user, hai request, hai PoP khác nhau: không nhất quán.
Điều này làm KV không hợp với:
- Counter tăng dần (race condition + giới hạn tốc độ ghi).
- Session cần strong consistency (user login ở Sydney, request 2 giây sau ở Tokyo thấy chưa login).
- Primary database (stale read = logic nghiệp vụ hỏng).
cacheTtl: controlled staleness
Muốn đánh đổi consistency lấy hiệu năng:
// Cache 5 phút trong edge, ngay cả khi key đã hết hạn ở central
const cached = await env.CACHE.get("heavy-config", { cacheTtl: 300 });
cacheTtl > 60s: đọc siêu nhanh, chấp nhận stale lâu hơn.
cacheTtl: 0: luôn đi về central (chậm, đắt, mất lợi thế KV).
Blog này dùng cacheTtl: 60 cho tính năng cờ. Nếu đổi cờ, tối đa 1 phút để edge phản ánh.
Giới hạn cần thuộc
Tốc độ ghi per key: 1/giây
Quan trọng nhất. Thử:
for (let i = 0; i < 10; i++) {
await env.CACHE.put("counter", String(i));
}
Lần ghi đầu thành công. 9 lần ghi sau bị throttle, một số có thể bị drop. KV không phải counter.
Counter thực tế dùng Durable Object (Part 15) hoặc D1 (UPDATE ... SET count = count + 1).
Kích thước value: 25 MB
Blob lớn hơn phải chia hoặc dùng R2. Với 25MB thực tế đã quá lớn cho KV vì chi phí phụ cache edge.
Kinh nghiệm: value KV nên < 1MB. Càng nhỏ càng nhanh.
Metadata: 1 KB
Metadata là trường phụ đi kèm key, lấy được mà không cần lấy value. Rất hữu ích cho list():
await env.POSTS.put(slug, body, {
metadata: { published: true, tags: ["cf"], reading_time: 5 },
});
const result = await env.POSTS.list({ prefix: "2026/" });
const publishedPosts = result.keys.filter(k => k.metadata?.published);
Không cần gọi get() cho mỗi key. Lọc ngay tại list().
List: 1000 item per page
let cursor: string | undefined;
const all: KVNamespaceListKey<any>[] = [];
do {
const page = await env.KV.list({ prefix: "user:", cursor, limit: 1000 });
all.push(...page.keys);
cursor = page.list_complete ? undefined : page.cursor;
} while (cursor);
Nếu namespace có 100k key, list() hết qua 100 trang. Mỗi trang 1 subrequest.
5 pattern đúng
① Tính năng cờ
// Cấu hình đổi ít, đọc nhiều
async function isFeatureEnabled(env: Env, feature: string, userId: string) {
const config = await env.FLAGS.get("features", { type: "json", cacheTtl: 60 });
const rule = config?.[feature];
if (!rule) return false;
if (rule.enabled === true) return true;
if (rule.enabled === false) return false;
if (rule.allowlist?.includes(userId)) return true;
if (rule.rollout && hashToPercent(userId) < rule.rollout) return true;
return false;
}
Ghi qua dashboard hoặc API admin hiếm khi, đọc ở mỗi request. Stale 60s chấp nhận được. Cổ điển KV.
② Redirect map / URL alias
// /go/twitter → https://twitter.com/khavan
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/go/")) {
const slug = url.pathname.slice(4);
const target = await env.REDIRECTS.get(slug);
if (target) return Response.redirect(target, 302);
}
}
Ghi thỉnh thoảng (thêm slug mới), đọc ở click. KV hợp hoàn hảo.
③ Session token ngắn hạn
// Session 24h, token → userId
async function createSession(env: Env, userId: string) {
const token = crypto.randomUUID();
await env.SESSIONS.put(token, userId, { expirationTtl: 86400 });
return token;
}
async function verifySession(env: Env, token: string) {
return await env.SESSIONS.get(token);
}
KV tự hết hạn bằng TTL, không cần cron dọn dẹp. Ghi 1 lần khi login, đọc ở mỗi request đã xác thực.
Lưu ý: Session cần strong consistency (user logout → không vào được ngay) thì không dùng KV. Dùng D1 hoặc Durable Object. Session “kéo dài session” mà logout chậm 60s OK thì KV ổn.
④ Asset metadata / content lookup
// Blog này: OIDC creds temp cache
async function getCachedAwsCreds(env: Env, arn: string) {
const cached = await env.OIDC_CREDS_CACHE.get(`creds:${arn}`, { type: "json" });
if (cached && cached.expiresAt > Date.now() + 300_000) {
return cached;
}
const fresh = await assumeRoleWithWebIdentity(env, arn);
await env.OIDC_CREDS_CACHE.put(`creds:${arn}`, JSON.stringify(fresh), {
expirationTtl: Math.floor((fresh.expiresAt - Date.now()) / 1000) - 60,
});
return fresh;
}
Credentials hợp lệ 15-60 phút. Cache giúp tránh gọi STS mỗi request. Hết hạn tự động.
⑤ Cấu hình giới hạn tốc độ
// Config rate limit per endpoint, đọc ở mỗi request
async function getRateLimit(env: Env, endpoint: string) {
const config = await env.RATE_LIMITS.get("limits", { type: "json", cacheTtl: 300 });
return config?.[endpoint] ?? { rpm: 60, burst: 100 };
}
Counter thực tế (số request / phút) vẫn cần Durable Object. KV chỉ giữ ngưỡng.
3 anti-pattern
① Counter / metric tăng dần
// SAI — sẽ throttle, sẽ mất update
async function incrementPageView(env: Env, slug: string) {
const current = Number(await env.VIEWS.get(slug)) ?? 0;
await env.VIEWS.put(slug, String(current + 1));
}
2 vấn đề:
- Race condition: đọc-sửa-ghi không nguyên tử. 2 request song song = mất update.
- Tốc độ ghi: trang phổ biến = hàng trăm view/giây = throttle ngay.
Đúng: dùng Durable Object với storage.transaction() hoặc D1 với UPDATE ... SET count = count + 1 (D1 primary là SQLite, nguyên tử).
② Primary database cho user / post / order
// SAI — stale, không nhất quán
async function getUser(env: Env, id: string) {
return await env.USERS.get(id, { type: "json" });
}
async function updateUser(env: Env, id: string, user: User) {
await env.USERS.put(id, JSON.stringify(user));
}
Tưởng tượng: user cập nhật profile ở PoP SIN, ngay sau đó tải profile ở PoP LAX → thấy bản cũ 30s. User bối rối, ticket hỗ trợ tăng.
Đúng: D1 cho relational, nguồn chân lý. KV chỉ cache dẫn xuất.
③ Session với logout cần hiệu lực ngay
// SAI nếu logout phải vô hiệu hóa cứng
async function logout(env: Env, token: string) {
await env.SESSIONS.delete(token);
}
async function verifySession(env: Env, token: string) {
// Có thể thấy session cũ đến 60s sau khi xóa
return await env.SESSIONS.get(token);
}
User click logout → vẫn dùng được token 30s sau. Vấn đề an ninh.
Đúng: Durable Object cho session hoặc D1 với đọc mạnh. Hoặc thiết kế session TTL ngắn (15 phút) để dù stale cũng hết hạn nhanh.
Gotcha từ blog này
① Mất update OIDC creds khi đồng thời
Ban đầu:
// SAI
async function getCachedCreds(env: Env) {
const cached = await env.KV.get("aws-creds");
if (cached && !expired(cached)) return cached;
const fresh = await assumeRole();
await env.KV.put("aws-creds", fresh);
return fresh;
}
Vấn đề: 2 request song song cùng miss cache, cả 2 gọi STS, cả 2 ghi KV. AWS STS tính phí 2 lần, không sao, nhưng tốc độ ghi đạt nhẹ.
Sửa: chấp nhận lấy 2 lần (giờ vẫn vậy, không quan trọng), hoặc dùng Durable Object làm singleton locker (quá phức tạp cho value chỉ mất $0.001).
② Metadata 1KB bị cắt
// SAI — metadata > 1KB bị cắt âm thầm
await env.KV.put(key, value, {
metadata: {
tags: longArray, // 2KB → cắt
description: longText,
},
});
Metadata >1KB không lỗi, bị cắt. Kiểm tra kích thước trước khi put.
③ list() prefix phân biệt hoa thường
await env.KV.put("User:alice", "...");
await env.KV.put("user:bob", "...");
const result = await env.KV.list({ prefix: "user:" });
// Chỉ trả "user:bob", không trả "User:alice"
Chọn quy ước (chữ thường ưu tiên) và giữ vững.
④ Binary value qua JSON serialize
// SAI
const data = new Uint8Array([1, 2, 3]);
await env.KV.put("key", JSON.stringify(data)); // "{}" không phải binary
// ĐÚNG
await env.KV.put("key", data); // KV tự xử lý ArrayBuffer
const back = await env.KV.get("key", { type: "arrayBuffer" });
Production checklist
- Mọi
get()có TTL ngầm phù hợp với staleness chấp nhận được. - Không dùng KV cho counter / tăng metric.
- Không dùng KV làm primary DB cho user / post / order.
- Kích thước value < 1MB cho hot key; > 1MB cân nhắc R2.
- Metadata < 1KB sau JSON serialize.
- Session quan trọng (logout cần hiệu lực) không dùng KV.
-
list()có phân trang cursor nếu namespace > 1000 key. - Tốc độ ghi per key < 1/giây (dùng DO/D1 cho ghi thường xuyên).
- Quy ước hoa thường cho key (thường chữ thường).
Khi nào KHÔNG dùng KV
Quyết định đơn giản:
- Cần strong consistency → D1 hoặc Durable Object.
- Cần ghi nhanh liên tục / key → Durable Object.
- Value binary / media > 1MB → R2.
- Dữ liệu quan hệ với JOIN → D1.
- Queue / pub-sub → Queues / Durable Object.
KV hợp nhất cho: metadata, cấu hình, cache, token ngắn hạn — đọc nhiều, ghi ít, stale OK.
Kết
KV là cache toàn cầu, không phải database. Mô hình tư duy: central store + edge cache + lan truyền ≤ 60s. Tốc độ ghi giới hạn ở 1/key/giây vì kiến trúc này. Đọc rẻ vì hit cache PoP local.
5 pattern đúng: tính năng cờ, redirect map, session ngắn hạn, asset metadata, cấu hình giới hạn tốc độ. 3 anti-pattern chính: counter, primary DB, session cần vô hiệu hóa cứng.
Part 6 tới đi vào D1: relational database thực sự trên edge, SQL, transaction, FTS, migration pattern.