Caching
How PgBeam caches query results at the edge, when it bypasses cache, and how to turn it on safely for your workload.
PgBeam caches query results at the data plane level. When a cached result is available, PgBeam returns it directly without touching the upstream database. This reduces upstream load, lowers read latency, and saves you money on database compute.
Each region keeps its own independent cache. There is no cross-region coherence layer. That is a deliberate tradeoff. Synchronizing caches across regions would add latency to every read, which defeats the purpose of caching.
Caching is opt-in
Caching starts disabled for new databases. This is the safe default. Turn it on after traffic is flowing and you know which reads are stable enough to benefit from caching.
When caching helps
Caching is most effective when your workload has repeated reads that return the same data across many requests. Common examples:
- Product catalogs and category listings
- Configuration tables read on every request
- User profile lookups that change infrequently
- Leaderboards, stats, and aggregation queries that can tolerate staleness
- Reference data (countries, currencies, feature flags)
Caching is not a good fit for:
- Queries that must always return the latest data (e.g., account balances)
- Write-heavy workloads where data changes between reads
- Queries with highly variable parameters that produce unique results each time
How caching works
Query classification
PgBeam classifies each incoming query. Read statements (SELECT) are
candidates for caching. Writes (INSERT, UPDATE, DELETE), DDL
(CREATE TABLE, ALTER), session-changing commands (SET, DISCARD), and
queries with volatile functions (NOW(), RANDOM(), pg_advisory_lock()) are
never cached.
Cache key generation
For cacheable queries, PgBeam generates a cache key from the normalized SQL text plus parameter values, scoped to the project. Normalization means whitespace and casing differences do not create separate cache entries.
The same query template with different parameter values produces different cache entries:
-- These produce two separate cache entries
SELECT * FROM products WHERE id = 1;
SELECT * FROM products WHERE id = 2;Cache lookup
PgBeam checks the local cache and then the regional shared cache for a matching entry:
- Hit: A fresh cached result is returned immediately. The upstream database is never contacted.
- Stale hit: The cached result has passed its TTL but is within the stale-while-revalidate (SWR) window. PgBeam returns the stale result immediately and refreshes the cache in the background.
- Miss: No cached result exists. The query goes to the upstream database, and the result is stored in the cache for future requests.
Cache defaults
| Setting | Default | What it controls |
|---|---|---|
| TTL | 60s | How long a cached result is considered fresh |
| SWR | 30s | Extra window after TTL where stale results can be served while refreshing |
| Max entries | 10,000 | Approximate upper bound of cached entries per project |
| Enabled | false | Caching is off by default for new databases |
Understanding TTL and SWR
TTL and SWR work together to balance freshness and performance:
◀──── TTL (60s) ────▶◀── SWR (30s) ──▶
│ fresh hit │ stale hit │ miss
│ (instant return) │ (instant return │ (upstream query,
│ │ + bg refresh) │ cache updated)
┼────────────────────┼────────────────┼──────────
cache write TTL expires SWR expires- During the TTL window, cached results are returned without any upstream query.
- During the SWR window, the stale result is returned immediately (keeping latency low), and PgBeam refreshes the entry in the background.
- After the SWR window, the entry is treated as a miss and the upstream is queried.
Invalidation: writes do not auto-clear the cache
Cached reads are cleared by TTL, not by writes
PgBeam does not automatically invalidate cached results when a write flows
through the proxy. Writes are never cached, but a SELECT cached before the
write keeps being served until its TTL (and SWR window) expire. Plan for a
staleness window of up to TTL + SWR after any change.
If you need cached data cleared sooner than its TTL, opt in to event-based
invalidation. PgBeam listens on the pgbeam_invalidate channel on your upstream
database and evicts cached entries for a table when it receives a notification
naming that table. You emit those notifications from triggers you install on the
tables you cache:
CREATE OR REPLACE FUNCTION pgbeam_notify_invalidate() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('pgbeam_invalidate', TG_TABLE_NAME);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER products_invalidate
AFTER INSERT OR UPDATE OR DELETE ON products
FOR EACH STATEMENT
EXECUTE FUNCTION pgbeam_notify_invalidate();The NOTIFY payload is the table name (bare or schema-qualified). PgBeam evicts
the cached entries that read from that table.
NOTIFY needs a direct upstream, not a pooler
LISTEN/NOTIFY requires a session-level connection. PgBeam opens one direct
session connection to your upstream to listen on pgbeam_invalidate. If the
upstream connection details you gave PgBeam point at a PgBouncer-style
transaction pooler (PlanetScale, the Supabase pooler, RDS Proxy), the LISTEN
silently receives nothing and invalidation does nothing, with no error. Point
PgBeam's upstream at a direct, session-capable endpoint (not the pooler port)
if you want NOTIFY invalidation to work.
If a table changes often and must never be served stale, do not rely on
invalidation timing. Either skip the cache for those reads with
/* @pgbeam:cache noCache */, or give them a very short TTL (for example
/* @pgbeam:cache maxAge=1 */) so the freshness window is bounded regardless of
whether a notification arrives.
Ways to control cache behavior
PgBeam checks cache controls in priority order. The first match wins:
| Priority | Source | Example | Scope |
|---|---|---|---|
| 1 | SQL annotation | /* @pgbeam:cache noCache */ | Per query |
| 2 | Session override off | SET pgbeam.cache = off | Per session |
| 3 | Cache rule | Dashboard rule for query shape | Per query shape |
| 4 | Session override on | SET pgbeam.cache = on | Per session |
| 5 | Database default | Database-level cache setting | All queries |
| 6 | Fallback | Off | n/a |
This precedence lets you set broad defaults and override them precisely where needed.
SQL annotations
Embed cache directives directly in your SQL as comments. PgBeam strips the annotations before forwarding to the upstream, so your database never sees them.
/* @pgbeam:cache */ SELECT * FROM products WHERE active = true;@pgbeam:cache maxAge=300 swr=60 */ SELECT * FROM products; ```
</Tab>
<Tab value="Disable caching">
```sql -- Force this query to always hit the upstream /* @pgbeam:cache noCache
*/ SELECT NOW(); ```
</Tab>
<Tab value="Route to replica">
```sql
/* @pgbeam:replica */ SELECT * FROM events ORDER BY created_at DESC LIMIT 100;Annotation reference
| Annotation | Effect |
|---|---|
/* @pgbeam:cache */ | Enable caching with default TTL and SWR |
/* @pgbeam:cache maxAge=N */ | Cache for N seconds |
/* @pgbeam:cache swr=N */ | Set SWR window to N seconds |
/* @pgbeam:cache maxAge=N swr=M */ | Set both TTL and SWR |
/* @pgbeam:cache noCache */ | Bypass cache for this query |
/* @pgbeam:replica */ | Route to a read replica |
Session overrides
Control caching for an entire connection session. Useful for debugging or for application code that needs to temporarily force cache behavior.
-- Enable caching for this session
SET pgbeam.cache = on;
-- Set TTL for this session
SET pgbeam.cache_ttl = 300;
-- Enable debug output (see cache hit/miss notices)
SET pgbeam.debug = on;
-- Check current settings
SHOW pgbeam.cache;
SHOW pgbeam.cache_ttl;
SHOW pgbeam.debug;When pgbeam.debug is on, every query returns a NOTICE with cache
information:
SET pgbeam.debug = on;
SELECT * FROM users WHERE id = 1;
-- NOTICE: pgbeam:cache hit age=12s ttl=1m0s swr=30sCache bypasses
PgBeam skips the cache entirely when any of these conditions are true:
| Condition | Why it bypasses |
|---|---|
| Caching not enabled for the query | No rule, annotation, or session override |
| Statement mutates data | INSERT, UPDATE, DELETE, TRUNCATE |
| Statement changes session state | SET, DISCARD, RESET |
| Query uses volatile functions | NOW(), RANDOM(), pg_advisory_lock(), etc. |
| Query is inside an explicit transaction | BEGIN ... COMMIT blocks |
SQL annotation says noCache | Explicit opt-out |
Session override is pgbeam.cache = off | Session-level opt-out |
Cache layers
PgBeam uses a two-layer cache for speed and resilience:
| Layer | Scope | Latency | Failure behavior |
|---|---|---|---|
| L1 | Per-process | Microseconds | Process-local; failure is extremely rare |
| L2 | Shared per region | Sub-millisecond | Fail-open: queries go to upstream |
L1 is checked first. On an L1 miss, L2 is checked. On an L2 miss, the query goes to the upstream database. Results from the upstream are written back to both layers.
If the shared cache is unavailable, PgBeam falls through to the origin database. Your application continues working. It just loses the caching benefit until the cache recovers.
Cache rules
Cache Rules give you a dashboard-based way to enable caching for specific query shapes without changing application code. PgBeam automatically tracks the distinct query shapes flowing through your project, so you can see what your traffic looks like before you start caching anything.
View cache rules
Open your database in the dashboard and go to Cache Rules. You will see a list of detected query shapes with frequency and timing data.
curl -H "Authorization: Bearer <api-key>" \
https://api.pgbeam.com/v1/projects/{projectId}/databases/{dbId}/cache-rulespgbeam projects cache-rules list --database-id <id>Enable caching for a query shape
Once you identify a high-frequency read query that would benefit from caching, enable it from the dashboard or API:
Find the query shape in Cache Rules and toggle caching on. You can optionally set a custom TTL and SWR for that shape.
curl -X PUT \
https://api.pgbeam.com/v1/projects/{projectId}/databases/{dbId}/cache-rules/{hash} \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"cache_enabled": true}'Recommended approach
- Let traffic flow for a while: PgBeam needs to see your query patterns before you can make informed caching decisions.
- Start with high-frequency, stable reads: Look for queries that run hundreds or thousands of times per day and return data that changes infrequently.
- Avoid caching queries with volatile data: If a query returns different results every time, caching wastes memory without saving upstream load.
- Monitor cache hit rates: Use the dashboard or Query Insights to see whether caching is actually helping.
Further reading
- Read Replicas: Route reads to replicas for load distribution
- Plans & Limits: Cache entry limits and defaults per plan tier
- Troubleshooting: Debugging "cache is not doing anything"
Connection Pooling
How PgBeam pools upstream PostgreSQL connections per project, keyed by user credentials, and how to choose the right pool mode for your workload.
Insights
Query-level analytics for a project. Top query shapes by call count, with cache hit rate and latency for each, plus aggregate cache and latency summaries.