Home/Fast Coding Skills/layerbase-valkey-nodejs-integration
Database8 min read0 downloads

Layerbase Valkey Integration Guide

Best practices for connecting Node.js and Bun apps to Layerbase Valkey with TLS SNI, anti-sleep heartbeats, and dynamic admin configuration.

#Valkey#Layerbase#Node.js#TLS#Caching
Descriptive File Name·0 downloads
layerbase-valkey-nodejs-integration.md

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 (or npm 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.
Save this skill as layerbase-valkey-nodejs-integration.md in your repository or AI editor config.