Build a Cache and Session Store with Valkey
A cache should reduce repeated work without becoming the only copy of important data. A session store should expire abandoned sessions without unexpectedly logging out active users.
Valkey has the Redis data structures and RESP protocol needed for both patterns. The code is short, but cache invalidation, TTL refresh, and migration compatibility still need deliberate decisions.
Who this is for: TypeScript developers evaluating Valkey for application caching and server-side sessions.
Outcome: One runnable script with cache-aside behavior and a session whose TTL refreshes on activity.
Time: About 15 minutes, plus the first Valkey binary download.
Prerequisites: Node.js 20 or newer and pnpm. No Docker or Cloud account is required.
Start Valkey locally
Install the Layerbase CLI, then create a Valkey instance:
npm i -g layerbase
lbase create valkey-patterns -e valkey --start
lbase url valkey-patternsValkey uses the Redis protocol, so the returned URL starts with redis://:
redis://127.0.0.1:6379/0If that port is occupied, use the URL printed by the CLI.
Create the TypeScript project:
mkdir valkey-patterns
cd valkey-patterns
pnpm init
pnpm add redis
pnpm add -D tsx typescriptThe redis package is a RESP client and works with Valkey for the commands used in this tutorial.
Build the cache and session store
Create valkey-patterns.ts:
import { createClient } from 'redis'
type Product = {
id: string
name: string
priceCents: number
}
function createValkeyClient() {
const rawUrl = process.env.VALKEY_URL ?? 'redis://127.0.0.1:6379'
const parsed = new URL(rawUrl)
if (parsed.protocol !== 'rediss:') {
return createClient({ url: rawUrl })
}
return createClient({
username: decodeURIComponent(parsed.username || 'default'),
password: decodeURIComponent(parsed.password),
socket: {
host: parsed.hostname,
port: Number(parsed.port || 6379),
tls: true,
servername: parsed.hostname,
},
})
}
const client = createValkeyClient()
client.on('error', (error) => console.error('Valkey error:', error))
await client.connect()
const CACHE_KEY = 'tutorial:product:42'
const SESSION_KEY = 'tutorial:session:abc-123'
await client.del(CACHE_KEY)
await client.del(SESSION_KEY)
const primaryProducts = new Map<string, Product>([
[
'42',
{
id: '42',
name: 'Mechanical keyboard',
priceCents: 12900,
},
],
])
let primaryReads = 0
async function loadFromPrimary(id: string): Promise<Product | null> {
primaryReads += 1
return primaryProducts.get(id) ?? null
}
async function getProduct(
id: string,
): Promise<{ product: Product | null; source: 'cache' | 'primary' }> {
const key = `tutorial:product:${id}`
const cached = await client.get(key)
if (cached) {
return {
product: JSON.parse(cached) as Product,
source: 'cache',
}
}
const product = await loadFromPrimary(id)
if (product) {
await client.set(key, JSON.stringify(product), { EX: 60 })
}
return { product, source: 'primary' }
}
const firstLookup = await getProduct('42')
const secondLookup = await getProduct('42')
console.log('Cache aside:')
console.log(` First lookup: ${firstLookup.source}`)
console.log(` Second lookup: ${secondLookup.source}`)
console.log(` Primary reads: ${primaryReads}`)
await client
.multi()
.hSet(SESSION_KEY, {
userId: '42',
role: 'admin',
lastActive: new Date().toISOString(),
})
.expire(SESSION_KEY, 1800)
.exec()
async function touchSession(key: string): Promise<void> {
await client
.multi()
.hSet(key, 'lastActive', new Date().toISOString())
.expire(key, 1800)
.exec()
}
await touchSession(SESSION_KEY)
const session = await client.hGetAll(SESSION_KEY)
const sessionTtl = await client.ttl(SESSION_KEY)
console.log('\nSliding session:')
console.log(` User: ${session.userId}`)
console.log(` Role: ${session.role}`)
console.log(` TTL refreshed: ${sessionTtl > 0}`)
await client.quit()The example uses an in-memory Map as the primary data source so it can prove cache behavior without presenting an artificial delay as a performance benchmark.
Run it with the URL returned by the CLI:
VALKEY_URL="$(lbase url valkey-patterns)" pnpm tsx valkey-patterns.tsExpected output:
Cache aside:
First lookup: primary
Second lookup: cache
Primary reads: 1
Sliding session:
User: 42
Role: admin
TTL refreshed: trueBoth keys start with tutorial: and are deleted at the beginning of the run. The script does not flush the database.
Make cache invalidation explicit
Cache-aside reads from Valkey first, loads from the primary store on a miss, then caches the result. Writes require a policy too.
A common sequence is:
- Commit the change to the primary database.
- Delete the corresponding cache key.
- Let the next read repopulate it.
Deleting before the primary write can allow another request to cache the old value. Updating both systems in one application request can still fail halfway through. For important data, use an outbox or change event so invalidation can be retried.
Popular keys can also create a cache stampede when they expire. Add small TTL jitter, coalesce concurrent loads, or refresh hot entries before expiration. Do not give every key the same expiration second under heavy traffic.
Decide whether sessions slide
The example refreshes the 30-minute TTL whenever touchSession runs. That creates a sliding idle timeout.
Some applications need an absolute maximum lifetime as well. Store a separate creation timestamp and reject the session after that deadline even if the idle TTL keeps moving.
Session records should contain identifiers and authorization context, not secrets that belong in a dedicated secret store. Delete the session on logout, rotate it after privilege changes, and decide whether losing Valkey data should log everyone out or whether sessions must be recoverable.
Redis compatibility has a version boundary
Valkey began as a Linux Foundation continuation of Redis OSS 7.2.4 and remains BSD 3-Clause licensed. Redis licensing has also changed since 2024: Redis 8 is offered under RSALv2, SSPLv1, or AGPLv3. The Redis license page has the current version-by-version details.
Existing Redis clients can commonly connect to Valkey without application-code changes because both speak RESP. That does not make every server feature, module, configuration file, or on-disk data file interchangeable.
The official Valkey migration guide documents physical compatibility with Redis OSS 7.2 and earlier. Redis Community Edition 7.4 and later produce data files that Valkey does not accept directly.
Before migrating:
- Record the Redis version, commands, modules, Lua scripts, key count, TTL distribution, and memory use.
- Test the application against Valkey before moving production data.
- Use a documented physical path only for compatible versions.
- Use a logical copy for incompatible data formats and verify values and TTLs.
- Freeze or account for writes during the final copy.
- Keep the source available until the target has passed application checks.
Rollback is simple only while the source remains current. If the application writes to Valkey after cutover, plan how those changes would return to Redis before calling the migration reversible.
Move the proven workflow to Layerbase Cloud
After the local behavior is correct, create Valkey on Layerbase Cloud and copy its rediss:// connection string from Quick Connect:
VALKEY_URL="rediss://default:password@your-host.cloud.layerbase.dev:6379" \
pnpm tsx valkey-patterns.tsThe script sends the database hostname as the TLS server name, which Layerbase Cloud requires for routing on the shared Valkey TLS port. Keep the password in server-side environment variables.
As verified on July 23, 2026, Valkey is available on the Free plan. Free databases sleep after 60 minutes of inactivity and wake on connect. That is useful for development and low-traffic applications. User sessions in a latency-sensitive production path should use an always-on paid configuration. Check current pricing before purchasing.
When Valkey is the wrong fit
Valkey is useful when the workload is mostly fast key lookups, counters, hashes, sets, or short-lived data. It is usually the wrong first choice when:
- The primary database already meets the latency target.
- Cached data has no reliable invalidation path.
- Sessions require durability that the deployment has not configured or tested.
- Relational queries or ad-hoc reporting are central requirements.
- The application relies on a Redis-specific module that has not been tested on Valkey.
For rate limiting and sorted sets, use the separate Redis rate limiter and leaderboard tutorial. For the product and governance decision, read Redis versus Valkey.
The Layerbase CLI is the fastest local path. Layerbase Desktop provides the same workflow in a GUI on macOS, Windows, and Linux. Move to Cloud after invalidation, session expiry, and migration behavior are tested.
Keep reading
- Add Redis or Valkey caching to a Lovable appLovable ships a browser SPA with no server, so a cache lives in a Supabase Edge Function, not your React code. Here is how to add a Redis-compatible Valkey the right way.
- Run Valkey on Windows (No Docker, No WSL)No official Valkey Windows binary exists anywhere. We manually compiled one so you can run Valkey natively on Windows without Docker or WSL.
- Migrating from Vercel KV to LayerbaseVercel KV was sunset and its stores moved to Upstash Redis, so the move is a data copy and a one-line client swap. Paste the rediss:// string behind your project Storage tab and copy every key, type, and TTL into flat-priced managed Redis or Valkey.
- Migrating from Upstash to LayerbaseUpstash bills per request, which is great at zero traffic and surprising at scale. Here is how to move to flat-priced managed Valkey on Layerbase: copy every key with one API key, and swap the REST client for a standard Redis driver.