Getting Started with Valkey: Sub-Millisecond Caching for Your Application
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"
}
}'
| Policy | Best For |
|---|---|
allkeys-lru | General caching (recommended) |
volatile-lru | Mixed workloads where some keys must never be evicted |
volatile-ttl | Prioritizing eviction of keys closest to expiry |
noeviction | Primary 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:
| Metric | Healthy Range | Action If Outside |
|---|---|---|
Hit rate (hits / (hits + misses)) | > 90% | Increase TTLs or pre-warm cache |
used_memory | < 80% of plan memory | Upgrade plan or lower TTLs |
evicted_keys | Low and steady | If spiking, you need more memory |
connected_clients | Below pool max | Increase 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:
- Read the Valkey configuration reference for persistence tuning and advanced parameters.
- Set up automated backups for disaster recovery.
- Explore connection string examples for other languages and frameworks.
- Try the CLI or TypeScript SDK for provisioning from your CI/CD pipeline.
Create a free FoundryDB account and provision your first Valkey instance in under 5 minutes.