name: layerbase-valkey description: >- Guide and best practices for integrating Layerbase Valkey (or any hosted/cloud Valkey) in Node.js and Bun applications. Use when connecting to Layerbase Valkey, configuring TLS SNI for wildcard certificates (*.cloud.layerbase.dev), fixing ETIMEDOUT or "Stream isn't writeable" connection errors, implementing anti-sleep keep-alive heartbeats, and enabling dynamic admin-panel configuration without restarts.
Layerbase Valkey Integration Guide
Valkey is an open-source, high-performance, drop-in replacement for Redis (forked from Redis 7.2.4 under the BSD license). Layerbase provides managed, serverless, and cloud Valkey instances with TLS and flat pricing.
This skill covers the end-to-end integration patterns, critical connection pitfalls, anti-sleep heartbeats, and dynamic admin panel management.
1. Client Library Selection
Use iovalkey (the official Valkey project client for Node.js and Bun):
bun add iovalkey
# or npm install iovalkey
iovalkey natively supports valkey://, valkeys://, redis://, and rediss:// schemes, handles cluster discovery, and provides the full Redis/Valkey command set with zero breaking changes.
2. Critical Connection Pitfalls & Solutions
Pitfall A: Missing TLS SNI (servername) Causes ETIMEDOUT
Layerbase hosts Valkey databases behind a multi-tenant TLS proxy with wildcard certificates (e.g. *.ovh2.cloud.layerbase.dev).
When connecting over rediss:// or valkeys://, the default Node/Bun TLS handshake does not always propagate the SNI hostname, causing the proxy to stall the handshake until ETIMEDOUT.
Fix: Always explicitly set tls: { servername: hostname } extracted from the URL:
export function getValkeyClientOptions(rawUrl: string) {
let hostname = "";
let isTls = false;
try {
const hasProtocol = /^[a-z][a-z\d+.-]*:\/\//i.test(rawUrl);
const parsed = new URL(hasProtocol ? rawUrl : `valkey://${rawUrl}`);
hostname = parsed.hostname;
isTls = parsed.protocol === "rediss:" || parsed.protocol === "valkeys:";
} catch {
// fallback
}
const options: Record<string, any> = {
connectTimeout: 10000,
maxRetriesPerRequest: 1,
retryStrategy(times: number) {
if (times > 3) return null;
return Math.min(times * 100, 1000);
},
};
if (isTls && hostname) {
options.tls = {
servername: hostname, // CRITICAL: Required for Layerbase wildcard TLS
};
}
return options;
}
Pitfall B: enableOfflineQueue: false Causes Instant 400 Errors
Never set enableOfflineQueue: false on clients that execute commands right after instantiation.
Because TLS and TCP sockets connect asynchronously, calling .ping(), .get(), or .set() before the socket is fully writable will immediately reject with:
Stream isn't writeable and enableOfflineQueue options is false
Fix: Leave enableOfflineQueue: true (default). It queues early commands and flushes them as soon as the socket handshake finishes. Use maxRetriesPerRequest: 1 so that genuine connection failures still fail fast.
3. Anti-Sleep Keep-Alive Heartbeat
Why Valkey Sleeps
Layerbase free and starter tier instances scale down or sleep after 5 to 10 minutes of idle inactivity. When sleeping, cold requests suffer extra latency while the container spins back up.
Solution 1: In-Process Heartbeat Loop (Persistent Servers / Docker)
Run a lightweight background PING every 3 minutes (180s) on long-running server instances:
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
export function startValkeyKeepAlive(intervalSeconds: number = 180): void {
if (heartbeatTimer) clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(async () => {
try {
const valkey = await getValkey();
if (valkey) {
await valkey.ping();
}
} catch (err) {
console.error("[valkey-keepalive] Heartbeat ping failed:", err);
}
}, intervalSeconds * 1000);
if (typeof heartbeatTimer.unref === "function") {
heartbeatTimer.unref();
}
}
Solution 2: Public Keep-Alive Cron Endpoint (Serverless / Vercel)
On serverless platforms (where Node processes freeze between requests), expose a lightweight ping route:
// GET /cron/valkey-keepalive
app.get("/cron/valkey-keepalive", async () => {
const start = Date.now();
const valkey = await getValkey();
if (!valkey) return { ok: false, error: "Valkey unconfigured" };
const reply = await valkey.ping();
return {
ok: true,
reply,
latencyMs: Date.now() - start,
timestamp: new Date().toISOString(),
};
});
Tip: Add this endpoint to a free uptime monitor (e.g. UptimeRobot, cron-job.org) set to ping every 4 minutes. This ensures zero downtime and keeps the database warm 24/7 at no cost.
4. Dynamic Admin Configuration & Hot-Reload
Rather than requiring environment variable edits and server redeploys, allow administrators to configure VALKEY_URL dynamically from the database / admin dashboard.
Client Factory Pattern with Hot-Reload (valkey.ts)
import Valkey from "iovalkey";
let valkeyClient: Valkey | null | undefined;
let currentUrl: string | null = null;
let lastCheckTime = 0;
const CHECK_INTERVAL_MS = 30_000;
export async function getValkey(): Promise<Valkey | null> {
const now = Date.now();
if (valkeyClient === undefined || now - lastCheckTime > CHECK_INTERVAL_MS) {
lastCheckTime = now;
const targetUrl = await resolveValkeyUrl(); // checks DB settings first, then env
if (targetUrl !== currentUrl || valkeyClient === undefined) {
updateActiveClient(targetUrl);
}
}
return valkeyClient ?? null;
}
export function updateActiveClient(targetUrl: string): Valkey | null {
if (valkeyClient) {
try { valkeyClient.disconnect(); } catch {}
valkeyClient = null;
currentUrl = null;
}
const cleanUrl = targetUrl.trim();
if (!cleanUrl) return null;
try {
const options = getValkeyClientOptions(cleanUrl);
const client = new Valkey(cleanUrl, options);
client.on("error", (err) => console.error("[valkey] Connection error:", err));
valkeyClient = client;
currentUrl = cleanUrl;
return client;
} catch (err) {
console.error("[valkey] Failed to initialize client:", err);
return null;
}
}
5. Fail-Open Shared Cache Pattern
Always implement a fail-open cache layer with an in-memory fallback. If Valkey experiences a momentary network blip, the application should fall back gracefully without breaking user requests.
const memoryCache = new Map<string, { value: unknown; expiresAt: number }>();
export async function cacheGet<T>(key: string): Promise<T | null> {
const valkey = await getValkey();
if (valkey) {
try {
const raw = await valkey.get(key);
if (raw === null || raw === undefined) return null;
return JSON.parse(raw) as T;
} catch (err) {
console.error(`[shared-cache] Valkey GET failed for ${key}:`, err);
return null; // Fail-open: cache miss on error
}
}
const entry = memoryCache.get(key);
if (!entry || entry.expiresAt <= Date.now()) {
memoryCache.delete(key);
return null;
}
return entry.value as T;
}
export async function cacheSet(key: string, value: unknown, ttlSeconds: number): Promise<void> {
const valkey = await getValkey();
if (valkey) {
try {
await valkey.set(key, JSON.stringify(value), "EX", ttlSeconds);
} catch (err) {
console.error(`[shared-cache] Valkey SET failed for ${key}:`, err);
}
return;
}
memoryCache.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
}
export async function cacheDelete(key: string): Promise<void> {
const valkey = await getValkey();
if (valkey) {
try { await valkey.del(key); } catch {}
return;
}
memoryCache.delete(key);
}
6. Connection Testing Utility
When providing connection testing in admin panels, always connect explicitly and capture both latency and engine version:
export async function testValkeyConnection(rawUrl: string) {
const start = Date.now();
const options = getValkeyClientOptions(rawUrl);
const client = new Valkey(rawUrl, {
...options,
lazyConnect: true,
connectTimeout: 8000,
});
try {
await client.connect();
const ping = await client.ping();
const latencyMs = Date.now() - start;
let version = "unknown";
let serverType = "Valkey";
try {
const info = await client.info("server");
const valkeyMatch = info.match(/valkey_version:([^\r\n]+)/);
const redisMatch = info.match(/redis_version:([^\r\n]+)/);
if (valkeyMatch) {
version = valkeyMatch[1];
serverType = "Valkey";
} else if (redisMatch) {
version = redisMatch[1];
serverType = "Redis";
}
} catch {}
client.disconnect();
return { success: true, latencyMs, version, serverType, message: `Connected in ${latencyMs}ms (${ping})` };
} catch (err) {
try { client.disconnect(); } catch {}
return { success: false, message: (err as Error).message };
}
}
7. Integration & Setup Checklist
- Install
iovalkey:bun add iovalkey(ornpm install iovalkey). - Configure
VALKEY_URL(format:rediss://default:<password>@<host>.cloud.layerbase.dev:6379). - Extract hostname and inject
tls: { servername: hostname }to support Layerbase's SNI proxy. - Implement anti-sleep heartbeat (3-minute interval) and expose
/cron/valkey-keepalive. - Wire up dynamic URL loading from database settings for admin panel management.
- Use fail-open fallback to memory cache if Valkey is unreachable.