PostgresCache
PostgresCache is a Cache implementation backed by PostgreSQL. It supports distributed cache invalidation via Postgres LISTEN/NOTIFY, making it suitable for multi-host deployments that already run Postgres and would rather not add Redis.
import { PostgresCache } from "@farbenmeer/tag-based-cache/postgres-cache";Requires the pg (node-postgres) package as a peer dependency.
Constructor
Section titled “Constructor”new PostgresCache(pool: Pool, options?: PostgresCacheOptions)- Parameters:
pool: ApgPool. The cache checks out a dedicated connection from this pool forLISTENwhile it has active subscribers, and short-lived connections for reads and writes. The pool’s lifecycle is owned by the caller.options.createSchema: Whentrue(the default) the required tables and indexes are created lazily on the first operation viaCREATE TABLE IF NOT EXISTS. Set tofalseif you manage the schema yourself (e.g. via migrations).options.schema: The Postgres schema that holds the cache tables. Defaults to whatever the connection’ssearch_pathresolves to (usuallypublic). When set, the tables are fully qualified as<schema>.cache_entries/<schema>.cache_tags, the schema is created if missing (unlesscreateSchemaisfalse), and a schema-specific invalidation channel is used so caches on different schemas never cross-notify.
import { Pool } from "pg";import { PostgresCache } from "@farbenmeer/tag-based-cache/postgres-cache";
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });
const cache = new PostgresCache(pool);
await cache.set({ key: "book:1", data: { title: "Dune" }, ttl: 300, tags: ["books", "book:1"],});
const entry = await cache.get("book:1");// { data: { title: "Dune" }, attachment: null }How It Works
Section titled “How It Works”Storage
Section titled “Storage”Entries are stored across two tables (qualified with the configured schema when set):
cache_entries— one row per key holding the JSONdata(asJSONB), the binaryattachment(asBYTEA), and theadded_at/expires_attimestamps (epoch milliseconds).cache_tags— a(key, tag)join table. Rows referencecache_entries(key)withON DELETE CASCADE, so a deleted entry drops its tag associations automatically.
get filters on expires_at so entries past their TTL are never returned, even before the garbage collector removes them.
Invalidation
Section titled “Invalidation”When delete(tags) is called:
- All entries associated with the given tags are deleted via the
cache_tagsjoin table (their tag rows cascade away). - An invalidation message is broadcast with
pg_notifyon thetag_based_cache_invalidatechannel so other connected instances are notified.
Subscriptions
Section titled “Subscriptions”subscribe checks out a dedicated pooled connection and issues LISTEN on the tag_based_cache_invalidate channel. The listener is set up lazily on the first subscribe call and the connection is released back to the pool when the last subscriber unsubscribes.
const unsubscribe = cache.subscribe((tags, meta) => { console.log("Invalidated:", tags);});
// Later:unsubscribe();Garbage Collection
Section titled “Garbage Collection”A background timer periodically deletes expired entries. The interval adapts between 5 seconds and 5 minutes based on how many rows each sweep removes, matching the InMemoryCache and FilesystemCache behaviour.
Shutdown
Section titled “Shutdown”Call close() to stop the garbage collector and release the dedicated listener connection back to the pool before shutting down. The underlying Pool is left untouched — end it yourself when appropriate.
await cache.close();await pool.end();When to Use
Section titled “When to Use”- Multi-host deployments where cache invalidation needs to propagate across processes and you already run Postgres.
- When you need the TApi Caching Strategies pub/sub system to work across multiple servers without introducing Redis.