Bỏ qua

KV Storage

KV là gì?

Global Key-Value Store

┌─────────────────────────────────────────────────────────────┐
│                    CLOUDFLARE KV                             │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────────────────────────────────────────┐       │
│  │              KV Namespace                        │       │
│  │                                                  │       │
│  │  key1 ──▶ value1                                │       │
│  │  key2 ──▶ value2                                │       │
│  │  key3 ──▶ value3                                │       │
│  │  ...                                            │       │
│  │                                                  │       │
│  └─────────────────────────────────────────────────┘       │
│                                                              │
│  Characteristics:                                            │
│  - Eventually consistent (reads may be stale)               │
│  - Optimized for read-heavy workloads                       │
│  - Global replication                                       │
│  - Up to 25MB per value                                     │
│  - TTL support                                              │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Create KV Namespace

Via Wrangler

# Create namespace
wrangler kv:namespace create MY_KV

# Output:
# Add the following to your wrangler.toml:
# [[kv_namespaces]]
# binding = "MY_KV"
# id = "xxxx"

# Create preview namespace (for dev)
wrangler kv:namespace create MY_KV --preview

# List namespaces
wrangler kv:namespace list

Configuration

# wrangler.toml

[[kv_namespaces]]
binding = "MY_KV"
id = "production-namespace-id"
preview_id = "preview-namespace-id"  # For wrangler dev

# Multiple namespaces
[[kv_namespaces]]
binding = "CACHE"
id = "cache-namespace-id"

[[kv_namespaces]]
binding = "SESSIONS"
id = "sessions-namespace-id"

Basic Operations

Get & Put

interface Env {
  MY_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // PUT - Store value
    await env.MY_KV.put('user:123', JSON.stringify({
      name: 'John',
      email: 'john@example.com',
    }));

    // GET - Retrieve value
    const value = await env.MY_KV.get('user:123');
    console.log(value); // '{"name":"John","email":"john@example.com"}'

    // GET as JSON
    const user = await env.MY_KV.get('user:123', 'json');
    console.log(user.name); // 'John'

    // GET as ArrayBuffer (for binary)
    const binary = await env.MY_KV.get('file:123', 'arrayBuffer');

    // GET as stream
    const stream = await env.MY_KV.get('large:123', 'stream');

    return new Response('OK');
  },
};

Delete

// Delete single key
await env.MY_KV.delete('user:123');

// No bulk delete via API - use wrangler
// wrangler kv:key delete --binding MY_KV key1

TTL (Time To Live)

Set Expiration

// Expires in 1 hour (seconds)
await env.MY_KV.put('session:abc', sessionData, {
  expirationTtl: 3600,
});

// Expires at specific timestamp
await env.MY_KV.put('token:xyz', tokenData, {
  expiration: Math.floor(Date.now() / 1000) + 86400, // 24 hours from now
});

// No expiration (default)
await env.MY_KV.put('config:app', configData);

Common TTL Values

const TTL = {
  MINUTE: 60,
  HOUR: 3600,
  DAY: 86400,
  WEEK: 604800,
  MONTH: 2592000,
};

// Cache API response for 1 hour
await env.CACHE.put(`api:${endpoint}`, response, {
  expirationTtl: TTL.HOUR,
});

// Session expires in 7 days
await env.SESSIONS.put(`session:${id}`, sessionData, {
  expirationTtl: TTL.WEEK,
});

Metadata

Store Metadata with Value

// Put with metadata
await env.MY_KV.put('user:123', userData, {
  metadata: {
    created: Date.now(),
    version: 1,
    type: 'user',
  },
});

// Get with metadata
const { value, metadata } = await env.MY_KV.getWithMetadata('user:123', 'json');

console.log(value);     // User data
console.log(metadata);  // { created: ..., version: 1, type: 'user' }

List Keys

Enumerate Keys

// List all keys
const { keys } = await env.MY_KV.list();
// keys = [{ name: 'key1', ... }, { name: 'key2', ... }]

// List with prefix
const { keys: userKeys } = await env.MY_KV.list({ prefix: 'user:' });

// Pagination
const { keys, list_complete, cursor } = await env.MY_KV.list({
  prefix: 'log:',
  limit: 100,
});

if (!list_complete) {
  // Get next page
  const { keys: moreKeys } = await env.MY_KV.list({
    prefix: 'log:',
    cursor,
  });
}

Common Patterns

Caching Pattern

async function getCachedData<T>(
  kv: KVNamespace,
  key: string,
  fetcher: () => Promise<T>,
  ttl: number
): Promise<T> {
  // Try cache first
  const cached = await kv.get(key, 'json');
  if (cached) {
    return cached as T;
  }

  // Fetch fresh data
  const data = await fetcher();

  // Store in cache
  await kv.put(key, JSON.stringify(data), {
    expirationTtl: ttl,
  });

  return data;
}

// Usage
const users = await getCachedData(
  env.CACHE,
  'users:all',
  async () => {
    const response = await fetch(`${SUPABASE_URL}/rest/v1/users`);
    return response.json();
  },
  3600  // 1 hour
);

Session Storage

interface Session {
  userId: string;
  email: string;
  createdAt: number;
}

async function createSession(env: Env, userId: string, email: string): Promise<string> {
  const sessionId = crypto.randomUUID();
  const session: Session = {
    userId,
    email,
    createdAt: Date.now(),
  };

  await env.SESSIONS.put(`session:${sessionId}`, JSON.stringify(session), {
    expirationTtl: 86400 * 7,  // 7 days
  });

  return sessionId;
}

async function getSession(env: Env, sessionId: string): Promise<Session | null> {
  const session = await env.SESSIONS.get(`session:${sessionId}`, 'json');
  return session as Session | null;
}

async function deleteSession(env: Env, sessionId: string): Promise<void> {
  await env.SESSIONS.delete(`session:${sessionId}`);
}

Rate Limiting

async function checkRateLimit(
  kv: KVNamespace,
  key: string,
  limit: number,
  windowSeconds: number
): Promise<{ allowed: boolean; remaining: number }> {
  const current = parseInt(await kv.get(key) || '0');

  if (current >= limit) {
    return { allowed: false, remaining: 0 };
  }

  await kv.put(key, (current + 1).toString(), {
    expirationTtl: windowSeconds,
  });

  return { allowed: true, remaining: limit - current - 1 };
}

// Usage in middleware
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
const { allowed, remaining } = await checkRateLimit(
  env.RATE_LIMIT,
  `rate:${ip}`,
  100,  // 100 requests
  60    // per minute
);

if (!allowed) {
  return new Response('Rate limit exceeded', { status: 429 });
}

Limits

KV Limits

┌─────────────────────────────────────────────────────────────┐
│                      KV LIMITS                               │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Free:                          Paid:                       │
│  - 100,000 reads/day            - 10M reads/month           │
│  - 1,000 writes/day             - 1M writes/month           │
│  - 1 GB storage                 - Pay per GB                │
│                                                              │
│  All plans:                                                  │
│  - Max key size: 512 bytes                                  │
│  - Max value size: 25 MB                                    │
│  - Max metadata size: 1024 bytes                            │
│  - Eventually consistent (up to 60s)                        │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Tổng kết

Operations

Operation Method
Read kv.get(key)
Write kv.put(key, value, options)
Delete kv.delete(key)
List kv.list({ prefix })
Read + Meta kv.getWithMetadata(key)

Use Cases

✅ Caching API responses
✅ Session storage
✅ Feature flags
✅ Rate limiting
✅ Configuration storage

Q&A

  1. Caching strategy hiện tại?
  2. Session storage ở đâu?
  3. Có cần rate limiting không?