Cache a PostgreSQL Lookup in Redis
Reduce repeated reference-data queries by caching PostgreSQL lookups in Redis. PostgreSQL remains the authoritative store, while expiring Redis keys serve recent values. Both clients execute in the same native Codon application; neither server needs a Codon extension.
Uses: PostgreSQL and Redis SDKs. Services: development PostgreSQL and Redis. Difficulty: advanced.
This is a bounded-staleness cache, not a transactionally consistent view across the two systems. Use authoritative reads for decisions that cannot tolerate an old price, such as final checkout or financial posting.
Prepare Development Connections
Use your normal libpq configuration to select a development PostgreSQL database
where your account can create temporary tables. Redis defaults to localhost port
6379; set REDIS_PORT or REDIS_SOCKET for another local test instance. The
program writes only a temporary PostgreSQL table and a uniquely prefixed Redis
key with a 60-second expiry. It deletes that key on successful completion.
No database superuser or server-side native module is needed. Do not point this local, unauthenticated Redis configuration at a production service. For remote connections, configure Redis ACLs and verified TLS and PostgreSQL certificate verification; keep credentials out of source code and command history.
Read Through the Cache
import os
import time
import exaloop.databases.postgres as postgres
from exaloop.databases.redis import Redis
def price(connection, cache, prefix: str, sku: str) -> Optional[int]:
key = prefix + sku
cached = cache.get(key, int)
if cached is not None:
print("redis", cached)
return cached
with connection.query("SELECT cents FROM cookbook_prices WHERE sku = $1", (sku,)) as result:
row = result.fetchone(Tuple[int])
if row is None:
return None
cents = row[0]
cache.set(key, cents, ex=60)
print("postgres", cents)
return cents
prefix = "cookbook:prices:" + str(os.getpid()) + ":" + str(time.time()) + ":"
with postgres.connect() as connection:
connection.execute("CREATE TEMP TABLE cookbook_prices (sku text PRIMARY KEY, cents bigint NOT NULL CHECK (cents >= 0))")
connection.execute("INSERT INTO cookbook_prices VALUES ($1, $2)", ("SKU-7", 12500))
with Redis(port=int(os.environ.get("REDIS_PORT", "6379")),
unix_socket_path=os.environ.get("REDIS_SOCKET", "")) as cache:
first = price(connection, cache, prefix, "SKU-7")
second = price(connection, cache, prefix, "SKU-7")
assert first == 12500 and second == 12500
assert price(connection, cache, prefix, "unknown") is None
cache.delete(prefix + "SKU-7")
exaloop --with postgres --with redis run -release catalog.codon
postgres 12500
redis 12500
The second lookup is a cache hit. An unknown SKU returns None; a zero price
would still be a valid cached value. SQL parameters are bound separately from
the query string. Redis writes set the value and expiry in one command.
Decide the Consistency Contract
In a real application, use a stable namespace containing tenant identity and the price-list or schema version. The unique prefix here isolates test runs; using it in production would prevent sharing cache entries. Apply authorization before the lookup so one tenant cannot retrieve another tenant's cached data.
Expiry alone allows stale data. A delayed reader can also populate a stale value after a concurrent database update and cache invalidation. For stronger rules, use immutable versioned keys and verify the authoritative version, or bypass the cache. A Redis transaction cannot make a PostgreSQL update atomic with a cache mutation.
This example propagates Redis failures rather than silently changing behavior. An optional-cache service may instead fall back to PostgreSQL on connection failures, with concurrency limits and monitoring to prevent a database overload. Do not catch every error: malformed cache values and SQL failures need their own policy. Likewise, do not retry an uncertain database write as though it were a failed read.
For a high-volume service, add hit/miss and latency metrics, bounded TTL jitter, and a policy for concurrent cache misses. Missing SKUs are not cached here; negative caching requires a distinct representation and its own expiry. One client connection belongs to one worker at a time; these SDKs do not supply a shared connection pool.
Related Guides
- PostgreSQL client transactions
- Redis pipelines and bulk reads
- MySQL client applications for an alternative authoritative store