Node Embedded SQLite: When Local Coordination Beats Redis Ops Cost

Node Embedded SQLite: When Local Coordination Beats Redis Ops Cost

Redis is great at shared state — and pricey when all you need is a per-host lease table, idempotency cache, or feature-flag snapshot. Node’s modern SQLite bindings (or better-sqlite3 on long-lived hosts) give you transactional local coordination without another cluster to page on.

⚡ TL;DR: Use embedded SQLite for single-node or sticky-session coordination, local idempotency, and read-mostly caches; keep Redis/Dynamo when state must be shared across tasks. Pair with Lambda Versioned Layers for native addons and Node.js Event Loop Lag p99.

When SQLite wins

Use case SQLite Redis
Per-process idempotency TTL Overkill
Host-local job leases (one ECS task) Optional
Multi-AZ shared sessions
Pub/sub fanout
Feature flag mirror (pull every 30s) ✅ if multi-writer

❌ Putting SQLite on Lambda /tmp as a multi-instance source of truth. ✅ Using it on a single worker or as a pure local cache with remote truth elsewhere.

Lean coordination schema

// lib/local-coord.ts — better-sqlite3 on ECS/K8s worker
import Database from "better-sqlite3";

const db = new Database("coord.db");
db.pragma("journal_mode = WAL");
db.exec(`
  CREATE TABLE IF NOT EXISTS leases (
    key TEXT PRIMARY KEY,
    owner TEXT NOT NULL,
    expires_at INTEGER NOT NULL
  );
`);

const acquireStmt = db.prepare(`
  INSERT INTO leases(key, owner, expires_at)
  VALUES (@key, @owner, @expires)
  ON CONFLICT(key) DO UPDATE SET
    owner = excluded.owner,
    expires_at = excluded.expires_at
  WHERE leases.expires_at < @now OR leases.owner = @owner
`);

export function tryAcquire(key: string, owner: string, ttlMs: number) {
  const now = Date.now();
  const info = acquireStmt.run({ key, owner, expires: now + ttlMs, now });
  return info.changes === 1;
}

Cache with explicit invalidation

db.exec(`CREATE TABLE IF NOT EXISTS kv (
  k TEXT PRIMARY KEY, v TEXT NOT NULL, exp INTEGER NOT NULL
)`);

export function cacheGet(k: string): string | null {
  const row = db.prepare(`SELECT v, exp FROM kv WHERE k = ?`).get(k) as
    | { v: string; exp: number }
    | undefined;
  if (!row) return null;
  if (row.exp < Date.now()) {
    db.prepare(`DELETE FROM kv WHERE k = ?`).run(k);
    return null;
  }
  return row.v;
}

For Lambda, prefer DynamoDB idempotency (Powertools) over /tmp SQLite unless you accept warm-only best-effort caches — see Lambda Powertools Structured Logs and warm-pool guidance.

Closing checklist

✅ Dos
– ✅ WAL mode; bounded DB file size; TTL sweeps
– ✅ Use for local leases/caches with clear ownership
– ✅ Keep remote system of record for multi-task state
– ✅ Match native addon ABI to runtime
– ✅ Backup/restore story if the file matters

❌ Don’ts
– ❌ Don’t multi-mount the same SQLite file across tasks
– ❌ Don’t replace Redis pub/sub with file locks
– ❌ Don’t ignore disk fill on noisy caches
– ❌ Don’t block the event loop with huge sync queries — keep statements small

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply