Skip to main content

Getting Started with Valkey: Sub-Millisecond Caching for Your Application

· 6 min read
FoundryDB Team
Engineering @ FoundryDB

Most applications hit a performance wall that has nothing to do with their code. The database query that takes 50ms works fine until you are serving 10,000 requests per minute and your connection pool is saturated. Adding an in-memory caching layer drops that response time to under a millisecond and takes the read load off your primary database.

Valkey is the open-source, Redis-compatible in-memory data store that the community rallied behind after Redis changed its license in 2024. It is wire-compatible with Redis, which means your existing Redis clients, libraries, and tooling work without modification. No license concerns, no vendor lock-in, and active development under the Linux Foundation.

This guide walks through provisioning a managed Valkey instance on FoundryDB and implementing common caching patterns in Python, Node.js, and Go.

Provision a Valkey Instance

A single API call creates a production-ready Valkey instance with TLS, authentication, and automated backups. This example uses the tier-2 plan (2 CPU, 4 GB memory) in the Stockholm zone:

curl -u YOUR_API_KEY: -X POST \
https://api.foundrydb.com/managed-services \
-H "Content-Type: application/json" \
-d '{
"name": "app-cache",
"database_type": "valkey",
"version": "8",
"plan_name": "tier-2",
"zone": "se-sto1",
"storage_size_gb": 20,
"storage_tier": "maxiops"
}'

The service is typically ready in under 3 minutes. Once it is running, retrieve your connection credentials:

# Get the connection host
curl -s -u YOUR_API_KEY: \
https://api.foundrydb.com/managed-services/{id} \
| jq '.dns_records[0].full_domain'

# Get the default user's password
curl -s -u YOUR_API_KEY: -X POST \
https://api.foundrydb.com/managed-services/{id}/database-users/default/reveal-password \
| jq '.password'

Valkey listens on port 6380 for TLS connections and 6379 for plaintext. Always use TLS in production.

Connecting with TLS

Every FoundryDB Valkey instance enforces TLS by default. Here is how to connect from each language.

Python (redis-py)

import redis

cache = redis.Redis(
host="app-cache.foundrydb.com",
port=6380,
username="default",
password="your-password",
ssl=True,
ssl_cert_reqs="required",
decode_responses=True,
)

cache.ping() # True

Node.js (ioredis)

import Redis from 'ioredis';

const cache = new Redis({
host: 'app-cache.foundrydb.com',
port: 6380,
username: 'default',
password: 'your-password',
tls: { rejectUnauthorized: true },
});

await cache.ping(); // "PONG"

Go (go-redis)

package main

import (
"context"
"crypto/tls"
"fmt"

"github.com/redis/go-redis/v9"
)

func main() {
ctx := context.Background()

cache := redis.NewClient(&redis.Options{
Addr: "app-cache.foundrydb.com:6380",
Username: "default",
Password: "your-password",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
})

pong, err := cache.Ping(ctx).Result()
fmt.Println(pong, err) // PONG <nil>
}

Caching Patterns

Three patterns cover the majority of caching use cases. Choose based on your consistency and performance requirements.

Cache-Aside (Lazy Loading)

The application checks the cache first. On a miss, it queries the database, then writes the result back to the cache. This is the most common pattern because it only caches data that is actually requested.

import json

def get_user(user_id: str) -> dict:
# Check cache first
cached = cache.get(f"user:{user_id}")
if cached:
return json.loads(cached)

# Cache miss: query the database
user = db.query("SELECT * FROM users WHERE id = %s", user_id)

# Store in cache with a 10-minute TTL
cache.setex(f"user:{user_id}", 600, json.dumps(user))
return user

The trade-off: the first request for any key is always slow (a cache miss), and stale data can persist until the TTL expires. For most read-heavy workloads, this is the right choice.

Write-Through

Every write to the database also writes to the cache. This eliminates stale reads at the cost of higher write latency, since every write now hits two systems.

def update_user(user_id: str, data: dict):
# Update database
db.execute("UPDATE users SET name=%s WHERE id=%s", data["name"], user_id)

# Update cache immediately
cache.setex(f"user:{user_id}", 600, json.dumps(data))

Use write-through when you cannot tolerate serving stale data, even briefly.

Session Storage

Valkey excels at session storage because sessions are naturally short-lived, key-value shaped, and need to be fast. Store the full session as a hash:

def create_session(session_id: str, user_id: str):
cache.hset(f"session:{session_id}", mapping={
"user_id": user_id,
"created_at": "2026-04-06T12:00:00Z",
"role": "admin",
})
cache.expire(f"session:{session_id}", 3600) # 1-hour session

def get_session(session_id: str) -> dict:
return cache.hgetall(f"session:{session_id}")

TTL Strategies and Eviction

Two mechanisms control memory usage: TTL (time-to-live) on individual keys, and the eviction policy that kicks in when memory is full.

Set TTLs based on data volatility. Frequently changing data (prices, inventory counts) should have short TTLs of 30 to 60 seconds. Rarely changing data (user profiles, product descriptions) can use TTLs of 10 to 60 minutes. Session tokens should match your application's session timeout.

Configure the eviction policy to allkeys-lru for caching workloads. The default policy, noeviction, returns errors when memory is full, which is correct for a primary data store but wrong for a cache. Update it via the configuration API:

curl -u YOUR_API_KEY: -X PATCH \
https://api.foundrydb.com/managed-services/{id}/configuration \
-H "Content-Type: application/json" \
-d '{
"parameters": {
"maxmemory-policy": "allkeys-lru"
}
}'
PolicyBest For
allkeys-lruGeneral caching (recommended)
volatile-lruMixed workloads where some keys must never be evicted
volatile-ttlPrioritizing eviction of keys closest to expiry
noevictionPrimary data store, not a cache

Monitoring Cache Hit Rates

A cache that is not being hit is just expensive memory. The two metrics that matter most are keyspace_hits and keyspace_misses. Together they give you your hit rate.

Pull these from the FoundryDB metrics API:

curl -s -u YOUR_API_KEY: \
"https://api.foundrydb.com/managed-services/{id}/metrics?metric=memory&period=1h" \
| jq '.metrics'

Key metrics to watch:

MetricHealthy RangeAction If Outside
Hit rate (hits / (hits + misses))> 90%Increase TTLs or pre-warm cache
used_memory< 80% of plan memoryUpgrade plan or lower TTLs
evicted_keysLow and steadyIf spiking, you need more memory
connected_clientsBelow pool maxIncrease pool size or add replicas

For production workloads, export metrics to your existing monitoring stack. FoundryDB supports pushing to Prometheus, Datadog, Grafana Cloud, and four other destinations via the metrics export configuration.

Adding High Availability

For production caches where downtime means degraded application performance, add a replica node. FoundryDB uses Sentinel to monitor both nodes and automatically promotes the replica if the primary fails. Failover completes in 5 to 15 seconds.

curl -u YOUR_API_KEY: -X POST \
https://api.foundrydb.com/managed-services/{id}/nodes \
-H "Content-Type: application/json" \
-d '{"role": "replica"}'

No client-side changes are required. The DNS record for your service automatically points to the current primary.

Next Steps

You now have a managed Valkey instance serving as a caching layer with TLS, eviction policies, and monitoring in place. From here:

Create a free FoundryDB account and provision your first Valkey instance in under 5 minutes.