Skip to main content

KV Store

The KV Store is an Invoke-specific persistent key-value storage system available globally as kv. It's project-scoped and supports TTL (time-to-live).

Overview

export default async function handler(req, res) {
// Store a value
await kv.set('user:name', 'Alice')

// Retrieve a value
const name = await kv.get('user:name')

// Check if exists
const exists = await kv.has('user:name')

// Delete a key
await kv.delete('user:name')

res.json({ name, exists })
}

API Methods

kv.get(key)

Retrieve a value by key. Returns null if key doesn't exist or has expired.

export default async function handler(req, res) {
const value = await kv.get('myKey')

if (value === null) {
return res.status(404).json({ error: 'Key not found' })
}

res.json({ value })
}

Returns: any - The stored value, or null if not found

Supported types:

  • Strings
  • Numbers
  • Booleans
  • Objects
  • Arrays
  • null

kv.set(key, value, ttl?)

Store a value with optional TTL (time-to-live) in milliseconds.

export default async function handler(req, res) {
// Permanent storage
await kv.set('user:123', { name: 'Alice', email: 'alice@example.com' })

// With TTL (expires in 1 hour)
await kv.set('session:abc', { userId: 123 }, 3600000)

// TTL in 5 minutes
await kv.set('temp:data', 'temporary', 300000)

res.json({ stored: true })
}

Parameters:

  • key (string) - The key to store under
  • value (any) - The value to store
  • ttl (number, optional) - TTL in milliseconds

Returns: Promise<void>

kv.has(key)

Check if a key exists (and hasn't expired).

export default async function handler(req, res) {
const hasUser = await kv.has('user:123')
const hasSession = await kv.has('session:abc')

res.json({ hasUser, hasSession })
}

Returns: Promise<boolean> - true if key exists, false otherwise

kv.delete(key)

Delete a key from the store.

export default async function handler(req, res) {
const deleted = await kv.delete('user:123')

res.json({
deleted, // true if key existed, false if not
message: deleted ? 'Key deleted' : 'Key not found'
})
}

Returns: Promise<boolean> - true if key was deleted, false if key didn't exist

kv.clear()

Delete all keys in the project's namespace.

export default async function handler(req, res) {
await kv.clear()
res.json({ message: 'All keys cleared' })
}

Returns: Promise<void>

Warning: This deletes ALL keys for the project. Use with caution!

Common Patterns

Simple Counter

export default async function handler(req, res) {
// Get current count
let count = (await kv.get('counter')) || 0

// Increment
count++

// Store new value
await kv.set('counter', count)

res.json({ count })
}

Session Management

import crypto from 'crypto'

export default async function handler(req, res) {
if (req.method === 'POST') {
// Create session
const sessionId = crypto.randomUUID()
const session = {
userId: req.body.userId,
createdAt: Date.now()
}

// Store for 24 hours
await kv.set(`session:${sessionId}`, session, 86400000)

res.json({ sessionId })
} else {
// Validate session
const sessionId = req.query.sessionId
const session = await kv.get(`session:${sessionId}`)

if (!session) {
return res.status(401).json({ error: 'Invalid or expired session' })
}

res.json({ userId: session.userId })
}
}

###Caching API Responses

export default async function handler(req, res) {
const cacheKey = `api:users:${req.query.id}`

// Check cache first
let user = await kv.get(cacheKey)

if (user) {
console.log('Cache hit')
return res.json({ user, cached: true })
}

// Cache miss - fetch from API
console.log('Cache miss')
const response = await fetch(`https://api.example.com/users/${req.query.id}`)
user = await response.json()

// Store in cache for 10 minutes
await kv.set(cacheKey, user, 600000)

res.json({ user, cached: false })
}

Rate Limiting

export default async function handler(req, res) {
const clientId = req.headers['x-client-id'] || req.ip
const rateLimitKey = `ratelimit:${clientId}`

// Get current request count
let requestCount = (await kv.get(rateLimitKey)) || 0

if (requestCount >= 100) {
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Maximum 100 requests per hour'
})
}

// Increment counter
requestCount++

// Set with 1 hour TTL
await kv.set(rateLimitKey, requestCount, 3600000)

// Set rate limit headers
res.set({
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': String(100 - requestCount)
})

res.json({ message: 'Success', requestCount })
}

Feature Flags

export default async function handler(req, res) {
const featureKey = 'feature:newUI'

// Check if feature is enabled
const isEnabled = await kv.get(featureKey)

if (isEnabled) {
res.json({ ui: 'new', message: 'New UI enabled' })
} else {
res.json({ ui: 'old', message: 'Old UI' })
}
}

// Admin endpoint to toggle feature
export async function admin(req, res) {
const { feature, enabled } = req.body
await kv.set(`feature:${feature}`, enabled)
res.json({ success: true })
}

User Preferences

export default async function handler(req, res) {
const userId = req.query.userId
const prefsKey = `prefs:${userId}`

if (req.method === 'GET') {
// Get preferences
const prefs = (await kv.get(prefsKey)) || {
theme: 'light',
language: 'en',
notifications: true
}

res.json({ preferences: prefs })
} else if (req.method === 'POST') {
// Update preferences
const currentPrefs = (await kv.get(prefsKey)) || {}
const newPrefs = { ...currentPrefs, ...req.body }

await kv.set(prefsKey, newPrefs)

res.json({ preferences: newPrefs })
}
}

Temporary Data Storage

export default async function handler(req, res) {
if (req.method === 'POST') {
// Generate share link
const shareId = crypto.randomUUID()
const data = req.body

// Store for 1 hour
await kv.set(`share:${shareId}`, data, 3600000)

res.json({
shareId,
expiresIn: 3600,
url: `${req.baseUrl}/share?id=${shareId}`
})
} else {
// Retrieve shared data
const shareId = req.query.id
const data = await kv.get(`share:${shareId}`)

if (!data) {
return res.status(404).json({ error: 'Share link expired or not found' })
}

res.json({ data })
}
}

Leaderboard

export default async function handler(req, res) {
const leaderboardKey = 'game:leaderboard'

if (req.method === 'POST') {
// Add score
const { player, score } = req.body

let leaderboard = (await kv.get(leaderboardKey)) || []
leaderboard.push({ player, score, timestamp: Date.now() })

// Keep top 10, sorted by score
leaderboard.sort((a, b) => b.score - a.score)
leaderboard = leaderboard.slice(0, 10)

await kv.set(leaderboardKey, leaderboard)

res.json({ leaderboard })
} else {
// Get leaderboard
const leaderboard = (await kv.get(leaderboardKey)) || []
res.json({ leaderboard })
}
}

Complex Data Structures

export default async function handler(req, res) {
const userId = req.query.userId
const cartKey = `cart:${userId}`

// Get cart
let cart = (await kv.get(cartKey)) || { items: [], total: 0 }

if (req.method === 'POST') {
// Add item
const { productId, quantity, price } = req.body

cart.items.push({ productId, quantity, price })
cart.total += quantity * price
cart.updatedAt = Date.now()

// Store for 7 days
await kv.set(cartKey, cart, 604800000)

res.json({ cart })
} else if (req.method === 'DELETE') {
// Clear cart
await kv.delete(cartKey)
res.json({ message: 'Cart cleared' })
} else {
// Get cart
res.json({ cart })
}
}

Best Practices

Namespacing Keys

Use prefixes to organize keys:

// User data
await kv.set('user:123:profile', {...});
await kv.set('user:123:settings', {...});

// Sessions
await kv.set('session:abc123', {...});

// Cache
await kv.set('cache:api:users:123', {...});

// Features
await kv.set('feature:darkMode', true);

Error Handling

export default async function handler(req, res) {
try {
const value = await kv.get('myKey')
res.json({ value })
} catch (error) {
console.error('KV error:', error)
res.status(500).json({ error: 'Storage error' })
}
}

Default Values

export default async function handler(req, res) {
// Provide defaults for missing keys
const settings = (await kv.get('settings')) || {
theme: 'light',
notifications: true
}

const counter = (await kv.get('counter')) || 0

res.json({ settings, counter })
}

TTL Management

// Short-lived data (5 minutes)
await kv.set('temp:data', value, 300000)

// Medium-lived data (1 hour)
await kv.set('cache:data', value, 3600000)

// Long-lived data (24 hours)
await kv.set('session:data', value, 86400000)

// Permanent data (no TTL)
await kv.set('permanent:data', value)

Limitations

Storage Quotas

  • Keys are project-scoped
  • Storage quota applies per project
  • Check your plan for specific limits

Value Size

  • Large values (>1MB) may impact performance
  • Consider compression for large data:
import zlib from 'zlib'

// Compress before storing
const data = { large: 'data' }
const compressed = zlib.gzipSync(JSON.stringify(data))
await kv.set('key', compressed.toString('base64'))

// Decompress when retrieving
const stored = await kv.get('key')
const buffer = Buffer.from(stored, 'base64')
const decompressed = JSON.parse(zlib.gunzipSync(buffer).toString())

TTL Precision

  • TTL is in milliseconds
  • Expiration may not be immediate (eventual consistency)
  • Don't rely on exact timing for critical operations

Important Notes

Project Scope

  • Each project has its own KV namespace
  • Keys are NOT shared between projects
  • Functions in the same project share the KV store

Async Operations

  • All KV operations are async and return Promises
  • Always use await or .then()
  • Handle errors appropriately

No Transactions

  • KV operations are not atomic
  • For counters with race conditions, implement locking:
// Simple implementation (may have race conditions)
let count = (await kv.get('counter')) || 0
await kv.set('counter', count + 1)

// Better: use timestamp-based versioning
const entry = (await kv.get('counter')) || { value: 0, version: 0 }
entry.value++
entry.version++
await kv.set('counter', entry)

Next Steps