Database

Key‑Value Database with Valkey

The Key‑Value profile is for data that is part of application state and cannot be treated as a disposable result. Storage is durable and the commercial plan uses the db-* products.

Use domain-specific prefixes and set a TTL only when the data should expire. NX is useful for conditional creation and short locks, but production locks need a unique token, an expiration, and safe release.

SET session:{user-id} <json> EX 3600
GET session:{user-id}
MSET profile:{user-id} <json> flags:{user-id} <json>
SET lock:{resource} <unique-token> NX EX 30

Do not use KEYS * in production. Prefer SCAN with bounded work and keep value sizes under control.

Node.js and TypeScript

npm install iovalkey
import Redis from 'iovalkey'
import { randomUUID } from 'node:crypto'

const source = process.env.VALKEY_URL
if (!source) throw new Error('VALKEY_URL is required')
const url = new URL(source)
const client = new Redis({
  host: url.hostname,
  port: Number(url.port),
  username: decodeURIComponent(url.username),
  password: decodeURIComponent(url.password),
  tls: { servername: url.hostname },
})

await client.set('session:user-42', JSON.stringify({ role: 'admin' }), 'EX', 3600)
const session = await client.get('session:user-42')
const created = await client.set('lock:invoice-42', randomUUID(), 'NX', 'EX', 30)
console.log({ session, lockCreated: created === 'OK' })
await client.quit()

Python

python -m pip install valkey
import json
import os
import uuid
from urllib.parse import urlparse
from valkey import Valkey

url = urlparse(os.environ["VALKEY_URL"])
client = Valkey(
    host=url.hostname, port=url.port, username=url.username, password=url.password,
    ssl=True, ssl_check_hostname=True, decode_responses=True,
)

client.set("session:user-42", json.dumps({"role": "admin"}), ex=3600)
session = client.get("session:user-42")
token = str(uuid.uuid4())
lock_created = client.set("lock:invoice-42", token, nx=True, ex=30)
print({"session": session, "lock_created": lock_created})
client.close()

Consistency and limits

  • Use a transaction or script when related operations must be atomic.
  • Make writes and retries idempotent; a lost network response does not reveal whether the server applied the operation.
  • Do not treat the profile as a relational database: there are no joins, constraints, or ad hoc queries.
  • Plan capacity for keys, values, application indexes, and growth margin.
  • Use the Cache profile when data can be rebuilt; do not use Key‑Value only to obtain storage.

Use namespaces by domain, such as session:, lock:, and counter:, to make inspection and selective invalidation easier. Set size limits for serialized values and prefer native structures when they reduce duplication. For counters, INCRBY is atomic on the server; for related fields, a Lua script or transaction can keep the operation consistent.

Locks need a TTL and a unique token. When releasing a lock, compare the token on the server so a worker does not remove a lock that another worker acquired after expiration. Sessions should expire in line with the authentication lifecycle and have a clear TTL-renewal strategy. On retries, use idempotency keys so a repeated request does not create duplicate effects.

The Key‑Value profile is not a relational database: ad hoc queries, joins, and reports belong in the appropriate system. Back up data that truly matters at the application level and test restoration; instance persistence does not remove the need for a recovery policy.

Next steps

On this page