Skip to main content

KV Store

New

Global key-value store with rich data structures, built-in rate limiting, and automatic TTL. Ship features that need fast state without managing infrastructure.

Key-Value Storage

Get, set, delete with JSON support and conditional writes

Data Structures

Hashes, lists, and sorted sets for complex state

Rate Limiting

Sliding window algorithm with one function call

Automatic TTL

Expiration in seconds, milliseconds, or timestamps

Overview

The KV Store provides a globally distributed key-value store with sub-millisecond latency. Store JSON values, implement rate limiting, build leaderboards with sorted sets, and manage user sessions — all without provisioning or managing infrastructure. Every key is automatically namespaced per app, ensuring complete data isolation.

Quick Start

Get started with key-value operations in seconds:

import { createKv } from '@sylphx/sdk/server'

const kv = createKv()

// Store a user profile with 1-hour TTL
await kv.set('user:123', { name: 'John', plan: 'pro' }, { ex: 3600 })

// Retrieve the profile
const { value, ttl } = await kv.get('user:123')
// value = { name: 'John', plan: 'pro' }, ttl = 3540

// Atomic page view counter
const views = await kv.incr('page:home:views')

// Rate limit API requests
const { success, remaining } = await kv.ratelimit('api:user:123', {
  limit: 100,
  window: '1h',
})

if (!success) {
  return new Response('Too Many Requests', { status: 429 })
}

Singleton Pattern

Use getKv() instead of createKv() for a singleton instance that reuses the same client across your application. Both read from SYLPHX_SECRET_KEY and SYLPHX_PLATFORM_URL environment variables.

Basic Operations

Core key-value operations with full JSON support and conditional writes:

Basic operations
import { getKv } from '@sylphx/sdk/server'

const kv = getKv()

// SET — store any JSON-serializable value
await kv.set('user:123', { name: 'John', score: 100 })

// SET with TTL — expires in 1 hour (3600 seconds)
await kv.set('session:abc', { userId: '123' }, { ex: 3600 })

// SET with millisecond precision TTL
await kv.set('lock:resource', true, { px: 5000 }) // 5 seconds

// SET with Unix timestamp expiration
await kv.set('promo:summer', { discount: 20 }, {
  exat: Math.floor(Date.now() / 1000) + 86400 // expires in 24h
})

// SET if Not eXists — only set if key is new
await kv.set('user:123:email', 'john@example.com', { nx: true })

// SET if eXists — only update existing keys
await kv.set('user:123:email', 'new@example.com', { xx: true })

// GET — returns value and remaining TTL
const { value, ttl } = await kv.get('user:123')
// value: { name: 'John', score: 100 }, ttl: null (no expiry)

// DELETE — returns number of keys removed (0 or 1)
const deleted = await kv.del('session:abc')

// EXISTS — check if a key exists
const exists = await kv.exists('user:123') // true

// EXPIRE — set or update TTL on existing key
await kv.expire('user:123', 7200) // expire in 2 hours

Key Namespacing

All keys are automatically prefixed with your app ID. A key like user:123 is stored as app:<appId>:user:123, ensuring complete isolation between applications.

Rate Limiting

Built-in sliding window rate limiting with a single function call. No need to implement your own algorithm or manage server-side scripts:

Rate limiting
import { getKv } from '@sylphx/sdk/server'

const kv = getKv()

// Basic API rate limiting — 100 requests per hour
const { success, remaining, limit, reset } = await kv.ratelimit(
  'api:user:123',
  { limit: 100, window: '1h' }
)

if (!success) {
  return new Response('Too Many Requests', {
    status: 429,
    headers: {
      'X-RateLimit-Limit': String(limit),
      'X-RateLimit-Remaining': String(remaining),
      'X-RateLimit-Reset': String(reset),
      'Retry-After': String(Math.ceil((reset - Date.now()) / 1000)),
    },
  })
}

// Different limits per tier
const limits = {
  free: { limit: 100, window: '1h' },
  pro: { limit: 1000, window: '1h' },
  enterprise: { limit: 10000, window: '1h' },
}

const result = await kv.ratelimit(
  `api:${userId}`,
  limits[userPlan]
)

// Login attempt limiting — 5 attempts per 15 minutes
const loginResult = await kv.ratelimit(
  `login:${email}`,
  { limit: 5, window: '15m' }
)

// Short burst limiting — 10 requests per 10 seconds
const burstResult = await kv.ratelimit(
  `burst:${ip}`,
  { limit: 10, window: '10s' }
)
Window FormatDescriptionExample
NsN seconds10s, 30s
NmN minutes1m, 15m
NhN hours1h, 24h
NdN days1d, 7d

Sliding Window Algorithm

Rate limiting uses a sliding window algorithm for accurate limiting. Unlike fixed windows that reset at boundaries, the sliding window continuously tracks requests, preventing burst abuse at window edges.

Data Structures

Beyond simple key-value pairs, the KV store supports rich data structures for more complex use cases:

import { getKv } from '@sylphx/sdk/server'

const kv = getKv()

// Store user profile as a hash — update individual fields
await kv.hset('user:123', {
  name: 'John Doe',
  email: 'john@example.com',
  plan: 'pro',
  loginCount: 42,
})

// Get a single field
const name = await kv.hget('user:123', 'name')
// 'John Doe'

// Get all fields
const profile = await kv.hgetall('user:123')
// { name: 'John Doe', email: 'john@example.com', plan: 'pro', loginCount: 42 }

// Update just one field without touching the rest
await kv.hset('user:123', { loginCount: 43 })

Hashes

Object-like storage. Update individual fields without rewriting the entire value.

User profiles, settings, metadata

Lists

Ordered collections. Push to head or tail, retrieve by range.

Activity feeds, queues, notifications

Sorted Sets

Scored members with automatic ordering. Query by rank or score range.

Leaderboards, priority queues, rankings

Batch Operations

Reduce round trips with multi-key operations. Get or set up to 100 keys in a single request:

Batch operations
import { getKv } from '@sylphx/sdk/server'

const kv = getKv()

// MSET — set multiple keys atomically
await kv.mset([
  { key: 'config:theme', value: 'dark' },
  { key: 'config:locale', value: 'en-US' },
  { key: 'config:timezone', value: 'America/New_York' },
])

// MSET with shared TTL
await kv.mset(
  [
    { key: 'cache:user:1', value: { name: 'Alice' } },
    { key: 'cache:user:2', value: { name: 'Bob' } },
  ],
  { ex: 300 } // all expire in 5 minutes
)

// MGET — get multiple keys in one round trip
const values = await kv.mget(['config:theme', 'config:locale', 'config:timezone'])
// { 'config:theme': 'dark', 'config:locale': 'en-US', 'config:timezone': 'America/New_York' }

// Missing keys return null
const mixed = await kv.mget(['exists:key', 'missing:key'])
// { 'exists:key': 'value', 'missing:key': null }

Batch Limits

Both mget and mset accept up to 100 keys per request. For larger datasets, split operations into multiple batches.

API Reference

Complete reference for all KV client methods:

MethodReturnsDescription
get(key){ value, ttl }Get value and remaining TTL
set(key, value, opts?)booleanSet value with optional TTL and NX/XX flags
del(key)numberDelete a key (returns 0 or 1)
exists(key)booleanCheck if key exists
incr(key, by?)numberAtomic increment (negative to decrement)
expire(key, seconds)booleanSet or update TTL on existing key
mget(keys)Record<string, T>Get up to 100 values in one request
mset(entries, opts?)voidSet up to 100 key-value pairs atomically
ratelimit(key, opts){ success, remaining, ... }Sliding window rate limiting
hset(key, fields)numberSet hash fields (returns created count)
hget(key, field)T | nullGet a single hash field
hgetall(key)Record<string, T> | nullGet all hash fields and values
lpush(key, ...values)numberPush to list head (returns new length)
lrange(key, start?, stop?)T[]Get list elements by index range
zadd(key, ...members)numberAdd scored members to sorted set
zrange(key, start?, stop?, opts?)KvZMember[]Get sorted set members by rank

Set Options

PropertyTypeDescription
exnumberExpire time in seconds
pxnumberExpire time in milliseconds
exatnumberUnix timestamp (seconds) at which the key will expire
pxatnumberUnix timestamp (milliseconds) at which the key will expire
nxbooleanOnly set if the key does not already exist
xxbooleanOnly set if the key already exists

Rate Limit Result

PropertyTypeDescription
successrequiredbooleanWhether the request is allowed (under the rate limit)
limitrequirednumberThe configured maximum number of requests
remainingrequirednumberRemaining requests in the current window
resetrequirednumberUnix timestamp (ms) when the rate limit resets

Best Practices

Use Descriptive Key Patterns

Follow a consistent naming convention like entity:id:field — for example user:123:profile, cache:api:response, counter:page:views

Always Set TTLs on Caches

Prevent stale data and unbounded memory growth by setting expiration on cached values. Use ex for seconds or px for millisecond precision.

Use NX for Distributed Locks

Combine SET with NX (not exists) and a TTL to implement safe distributed locks that auto-release on expiration.

Prefer Batch Operations

Use mget and mset instead of multiple individual calls. Each batch operation is a single network round trip, reducing latency.

Choose the Right Data Structure

Use hashes for objects with individual field access, lists for queues and feeds, sorted sets for ranked data like leaderboards.

Rate Limit at Multiple Layers

Apply rate limits at different granularities — per user, per IP, per endpoint — with different windows for burst vs sustained traffic.

Key Size Limits

Keys are limited to 512 characters. Values can be any JSON-serializable data. For large blobs, consider using the Storage service instead.