KV
Q
JOB
Queue & KV
async • cache • worker

Cách dùng Cloudflare Queues và KV cho side project

Dùng Cloudflare Queues xử lý job bất đồng bộ và Workers KV lưu cache/config, hợp dự án cá nhân chạy trên Workers hoặc Pages.

12 phút đọc16/06/2026

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 queue
  • message.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_retries lầ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