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
- Node.js Event Loop Lag p99
- Lambda Cold Starts on Node 20
- Lambda Powertools Node Structured Logs
- Lambda Warm Pools
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
