Tổng quan nhanh
Cloudflare cung cấp hai primitive quan trọng cho workload serverless ngoài D1 Database:
- Workers KV: key-value store phân tán toàn cầu — đọc nhanh, eventually consistent, phù hợp cho config, cache, session
- Cloudflare Queues: message queue — gửi job vào hàng đợi, Worker khác xử lý bất đồng bộ
Cả hai chỉ truy cập được qua binding trong Cloudflare Workers hoặc Pages Functions. Không có connection string, không có HTTP endpoint public, không dùng được từ server bên ngoài Cloudflare platform.
Free tier đủ dùng cho dự án cá nhân
Trước khi đi vào chi tiết, đây là điều đáng biết nhất: free tier của cả KV và Queue rộng rãi với side project và app nhỏ.
Ví dụ thực tế với Workers Free:
| Dự án | KV reads/ngày | KV writes/ngày | Queue ops/tháng |
|---|---|---|---|
| Blog cá nhân, 500 visitor/ngày | ~5.000 | ~10 | — |
| App gửi email welcome khi đăng ký, 50 user/ngày | ~500 | ~50 | ~1.500 |
| Internal tool 20 người dùng | ~2.000 | ~200 | ~5.000 |
| Giới hạn free | 100.000/ngày | 1.000/ngày | 1.000.000/tháng |
Hầu hết side project và app nội bộ nhỏ không chạm đến 10% giới hạn free. Chỉ khi scale lên vài nghìn user active mỗi ngày với nhiều write mới cần nghĩ đến Workers Paid ($5/tháng).
Khi nào hết free tier:
- KV write: app có nhiều thao tác ghi — mỗi update config, mỗi session mới là 1 write. App với 1.000+ active session mới/ngày sẽ gần giới hạn
- Queue: 1 triệu operations/tháng = ~33.000 message/ngày — cần traffic khá lớn mới chạm đến
Workers KV — key-value store toàn cầu
KV là gì
Workers KV là distributed key-value store chạy trên toàn bộ edge network của Cloudflare. Mỗi cặp key-value được replicate ra nhiều location — đọc từ bất kỳ edge nào đều nhanh.
KV không phải database. Không có query, không có index, không có relation. Chỉ có get, put, delete, list.
Free tier
| Workers Free | Workers Paid | |
|---|---|---|
| Reads/ngày | 100.000 | 10 triệu/tháng |
| Writes/ngày | 1.000 | 1 triệu/tháng |
| Storage | 1GB | 1GB ($0.50/GB thêm) |
| Giá | Miễn phí | $5/tháng |
Tạo KV namespace
wrangler kv namespace create MY_KV
# Output: id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Namespace riêng cho local dev
wrangler kv namespace create MY_KV --preview
Thêm vào wrangler.toml:
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
preview_id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
Dùng trong Workers và Pages
Trong Worker:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { pathname } = new URL(request.url);
// Đọc value
if (pathname === "/config") {
const value = await env.MY_KV.get("app_config");
return Response.json({ config: value });
}
// Ghi value
if (pathname === "/config" && request.method === "PUT") {
const body = await request.json<{ value: string }>();
await env.MY_KV.put("app_config", body.value);
return Response.json({ ok: true });
}
return new Response("Not found", { status: 404 });
},
};
Trong Pages Function (functions/api/config.ts):
export async function onRequestGet(context: EventContext<Env, string, unknown>) {
const value = await context.env.MY_KV.get("app_config");
return Response.json({ config: value });
}
TTL — tự xóa sau khoảng thời gian
// Lưu với TTL 1 giờ (3600 giây)
await env.MY_KV.put("session:abc123", JSON.stringify(sessionData), {
expirationTtl: 3600,
});
// Lưu với expiration timestamp tuyệt đối
await env.MY_KV.put("token:xyz", token, {
expiration: Math.floor(Date.now() / 1000) + 86400, // hết hạn sau 24h
});
Lưu object JSON
KV chỉ lưu string. Lưu object phải serialize/deserialize thủ công — hoặc dùng getWithMetadata để lưu cả metadata:
// Lưu
await env.MY_KV.put(
`user:${userId}`,
JSON.stringify({ name, email, role }),
{ expirationTtl: 3600 }
);
// Đọc
const raw = await env.MY_KV.get(`user:${userId}`);
const user = raw ? JSON.parse(raw) : null;
List keys
const list = await env.MY_KV.list({ prefix: "user:", limit: 100 });
// list.keys = [{ name: "user:1" }, { name: "user:2" }, ...]
// list.list_complete = false nếu còn key chưa lấy hết
KV phù hợp cho bài toán nào
Nên dùng KV:
- Feature flags, app config — đọc nhiều, ghi ít
- Session token, auth cache
- Rate limiting counter đơn giản (nhưng không chính xác 100% vì eventually consistent)
- Cache kết quả API bên ngoài với TTL
- Static data thường xuyên được đọc (danh sách quốc gia, currency...)
Không nên dùng KV:
- Data cần strong consistency — KV là eventually consistent, có thể đọc giá trị cũ trong vài giây
- Thay thế database — không query được, không sort được
- Counter chính xác tuyệt đối — dùng Durable Objects thay thế
Trường hợp áp dụng thực tế với Pages
Với app deploy trên Cloudflare Pages (Next.js, Astro, SvelteKit...), Pages Functions đọc KV trước khi trả về response — phù hợp cho data ít thay đổi nhưng cần đọc nhanh ở edge:
A/B testing và feature flags
Lưu config bật/tắt feature vào KV, Pages Function đọc trước khi render để quyết định trả về variant nào cho user:
// functions/api/config.ts
export async function onRequestGet(context: EventContext<Env, string, unknown>) {
const flags = await context.env.MY_KV.get("feature_flags");
const { newCheckout, betaDashboard } = JSON.parse(flags ?? "{}");
return Response.json({ newCheckout, betaDashboard });
}
Cache kết quả CMS hoặc API bên ngoài
Pages Function gọi Contentful, Notion, hoặc bất kỳ API nào tốn quota — lưu kết quả vào KV với TTL thay vì gọi lại mỗi request:
export async function onRequestGet(context: EventContext<Env, string, unknown>) {
const cacheKey = "homepage_posts";
const cached = await context.env.MY_KV.get(cacheKey);
if (cached) return Response.json(JSON.parse(cached));
const posts = await fetchFromCMS(); // gọi API bên ngoài
await context.env.MY_KV.put(cacheKey, JSON.stringify(posts), {
expirationTtl: 300, // cache 5 phút
});
return Response.json(posts);
}
Redirects động
Lưu bảng slug → URL vào KV, Pages Function đọc và redirect — không cần deploy lại code khi thêm redirect mới:
// functions/r/[slug].ts
export async function onRequestGet(context: EventContext<Env, "slug", unknown>) {
const { slug } = context.params;
const url = await context.env.MY_KV.get(`redirect:${slug}`);
if (url) return Response.redirect(url, 301);
return new Response("Not found", { status: 404 });
}
Thêm redirect mới chỉ cần:
wrangler kv key put --binding MY_KV "redirect:promo-tet" "https://yoursite.com/sale"
Rate limiting form submit
Đếm số lần submit theo IP trong KV, Pages Function check trước khi xử lý:
export async function onRequestPost(context: EventContext<Env, string, unknown>) {
const ip = context.request.headers.get("CF-Connecting-IP") ?? "unknown";
const key = `ratelimit:contact:${ip}`;
const count = parseInt((await context.env.MY_KV.get(key)) ?? "0");
if (count >= 5) {
return Response.json({ error: "Too many requests" }, { status: 429 });
}
await context.env.MY_KV.put(key, String(count + 1), { expirationTtl: 3600 });
// xử lý form...
return Response.json({ ok: true });
}
### KV không public ra ngoài
Tương tự D1, KV không có REST API public hay connection string. Để đọc/ghi KV từ ngoài Cloudflare platform:
```bash
# Đọc key qua Wrangler CLI
wrangler kv key get --binding MY_KV "app_config"
# Ghi key
wrangler kv key put --binding MY_KV "app_config" "production"
# List tất cả keys
wrangler kv key list --binding MY_KV --prefix "user:"
Cloudflare Queues — xử lý job bất đồng bộ
Queue là gì và tại sao cần
Trong serverless, Worker có thời gian chạy giới hạn (30 giây ở plan Free, 15 phút ở Paid). Với các tác vụ cần thời gian dài hoặc không cần trả kết quả ngay — gửi email, resize ảnh, gọi webhook, sync data — xử lý trực tiếp trong request handler là không hợp lý.
Queue giải quyết bằng cách tách producer và consumer:
Request → Worker (producer) → Queue → Worker (consumer) → xử lý job
↓
trả về response ngay
(không cần đợi job xong)
Free tier
| Workers Free | Workers Paid | |
|---|---|---|
| Operations/tháng | 1 triệu | 1 triệu đầu miễn phí, $0.40/triệu tiếp |
| Message size | 128KB | 128KB |
| Retention | 4 ngày | 4 ngày |
| Giá | Miễn phí | $5/tháng (Workers Paid) |
Tạo Queue
wrangler queues create my-queue
Cấu hình trong wrangler.toml:
# Producer binding — Worker này gửi message vào queue
[[queues.producers]]
binding = "MY_QUEUE"
queue = "my-queue"
# Consumer binding — Worker này nhận và xử lý message từ queue
[[queues.consumers]]
queue = "my-queue"
max_batch_size = 10 # xử lý tối đa 10 message mỗi lần
max_batch_timeout = 5 # hoặc đợi tối đa 5 giây nếu chưa đủ 10
max_retries = 3 # retry tối đa 3 lần nếu xử lý thất bại
dead_letter_queue = "my-queue-dlq" # queue chứa message lỗi sau khi hết retry
Gửi message (Producer)
Trong Worker hoặc Pages Function:
// Gửi một message
await env.MY_QUEUE.send({
type: "send_email",
to: "[email protected]",
subject: "Chào mừng",
templateId: "welcome",
});
// Gửi nhiều message cùng lúc
await env.MY_QUEUE.sendBatch([
{ body: { type: "resize_image", imageId: "img_1" } },
{ body: { type: "resize_image", imageId: "img_2" } },
{ body: { type: "resize_image", imageId: "img_3" } },
]);
Sau khi send() xong, Worker trả về response ngay — không đợi job được xử lý.
Xử lý message (Consumer)
Consumer Worker phải export handler queue:
export default {
// Handler cho HTTP request bình thường
async fetch(request: Request, env: Env): Promise<Response> {
return new Response("OK");
},
// Handler cho Queue message
async queue(batch: MessageBatch<JobMessage>, env: Env): Promise<void> {
for (const message of batch.messages) {
const job = message.body;
try {
if (job.type === "send_email") {
await sendEmail(job.to, job.subject, job.templateId);
message.ack(); // xác nhận xử lý thành công
} else if (job.type === "resize_image") {
await resizeAndUpload(job.imageId, env);
message.ack();
} else {
message.retry(); // đưa lại vào queue để retry
}
} catch (err) {
console.error("Job failed:", err);
message.retry(); // retry nếu xử lý lỗi
}
}
},
};
Ack và Retry
message.ack(): báo Cloudflare đã xử lý xong, xóa message khỏi queuemessage.retry(): trả message về queue để thử lại- Nếu không gọi
ack()trước khi handler kết thúc, Cloudflare tự retry - Sau
max_retrieslần thất bại, message được chuyển vào dead letter queue
Dead Letter Queue
DLQ chứa message không xử lý được sau tất cả các lần retry. Tạo queue riêng để monitor:
wrangler queues create my-queue-dlq
[[queues.consumers]]
queue = "my-queue-dlq"
max_batch_size = 1
// Consumer cho DLQ — ghi log hoặc alert
async queue(batch: MessageBatch, env: Env): Promise<void> {
for (const message of batch.messages) {
console.error("Dead letter:", JSON.stringify(message.body));
// gửi alert Slack, email, hoặc lưu vào D1 để investigate
message.ack();
}
}
Use case thực tế với Pages
Với app Next.js hoặc Astro deploy trên Cloudflare Pages, Pages Functions có thể gửi message vào Queue:
// functions/api/register.ts
export async function onRequestPost(context: EventContext<Env, string, unknown>) {
const { email, name } = await context.request.json();
// Lưu user vào D1
await context.env.DB.prepare(
"INSERT INTO users (email, name) VALUES (?, ?)"
).bind(email, name).run();
// Gửi job gửi welcome email vào Queue — không block response
await context.env.MY_QUEUE.send({
type: "welcome_email",
to: email,
name,
});
return Response.json({ ok: true }); // trả về ngay, không đợi email gửi xong
}
Queue không public ra ngoài
Queue không có endpoint HTTP để gửi message từ bên ngoài. Chỉ gửi được message qua binding trong Workers hoặc Pages Functions.
Nếu cần trigger job từ server bên ngoài, cần tạo một Worker endpoint nhận request rồi forward vào Queue:
// Worker làm "gateway" nhận request từ bên ngoài
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Xác thực request (secret header, JWT...)
const auth = request.headers.get("X-Internal-Secret");
if (auth !== env.INTERNAL_SECRET) {
return new Response("Unauthorized", { status: 401 });
}
const job = await request.json();
await env.MY_QUEUE.send(job);
return Response.json({ queued: true });
},
};
KV vs Queue vs D1 — chọn cái nào
| Bài toán | Dùng gì |
|---|---|
| Lưu config, feature flag, cache | KV |
| Session, auth token ngắn hạn | KV với TTL |
| Data có quan hệ, cần query | D1 |
| Gửi email, xử lý ảnh bất đồng bộ | Queue |
| Sync webhook, retry logic | Queue |
| Counter chính xác, distributed lock | Durable Objects |
| File, ảnh, video | R2 |