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.
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 | — |
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;-- Cache for 5 minutes, serve stale for 1 minute during refresh
/* @pgbeam:cache maxAge=300 swr=60 */ SELECT * FROM products;-- Force this query to always hit the upstream
/* @pgbeam:cache noCache */ SELECT NOW();/* @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=60s 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"