12 KiB
Query Caching Guide
Complete guide to Hyperdrive's automatic query caching.
Overview
Hyperdrive automatically caches read-only queries at Cloudflare's edge, reducing database load and improving response times.
Key Features:
- ✅ Automatic caching (enabled by default)
- ✅ Wire protocol parsing (differentiates reads from writes)
- ✅ Edge-based caching (near users)
- ✅ Zero configuration required
What Gets Cached
Cacheable Queries (✅ Cached)
PostgreSQL:
-- Simple SELECT
SELECT * FROM articles WHERE published = true ORDER BY date DESC LIMIT 50;
-- JOIN queries
SELECT u.name, p.title
FROM users u
JOIN posts p ON u.id = p.author_id
WHERE p.published = true;
-- Aggregates
SELECT COUNT(*) FROM users WHERE created_at > '2024-01-01';
-- Subqueries
SELECT * FROM products
WHERE category IN (SELECT id FROM categories WHERE active = true);
MySQL:
-- Simple SELECT
SELECT * FROM articles WHERE DATE(published_time) = CURDATE() ORDER BY published_time DESC LIMIT 50;
-- Aggregates
SELECT COUNT(*) as total_users FROM users;
-- JOIN queries
SELECT orders.id, customers.name
FROM orders
JOIN customers ON orders.customer_id = customers.id;
Non-Cacheable Queries (❌ NOT Cached)
Mutating Queries (writes to database):
-- INSERT
INSERT INTO users (name, email) VALUES ('John', 'john@example.com');
-- UPDATE
UPDATE posts SET published = true WHERE id = 123;
-- DELETE
DELETE FROM sessions WHERE expired = true;
-- UPSERT
INSERT INTO users (id, name) VALUES (1, 'John')
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;
-- CREATE/ALTER/DROP
CREATE TABLE new_table (...);
ALTER TABLE users ADD COLUMN age INT;
DROP TABLE old_table;
Volatile Functions (PostgreSQL):
-- LASTVAL() - returns last sequence value
SELECT LASTVAL(), * FROM articles LIMIT 50;
-- CURRVAL() - current sequence value
SELECT CURRVAL('users_id_seq');
-- NEXTVAL() - advance sequence
SELECT NEXTVAL('users_id_seq');
-- RANDOM() - random values
SELECT * FROM products ORDER BY RANDOM() LIMIT 10;
-- NOW(), CURRENT_TIMESTAMP with modifications
SELECT * FROM logs WHERE created_at > NOW() - INTERVAL '1 hour';
-- PG_SLEEP() - delays
SELECT PG_SLEEP(1);
Volatile Functions (MySQL):
-- LAST_INSERT_ID()
SELECT LAST_INSERT_ID(), * FROM articles LIMIT 50;
-- UUID() - generates random UUIDs
SELECT UUID();
-- RAND() - random values
SELECT * FROM products ORDER BY RAND() LIMIT 10;
-- NOW() with modifications
SELECT * FROM logs WHERE created_at > NOW() - INTERVAL 1 HOUR;
Transactions:
-- Any query within explicit transaction
BEGIN;
SELECT * FROM users; -- Not cached (within transaction)
COMMIT;
How Caching Works
1. Wire Protocol Parsing
Hyperdrive parses the database wire protocol (not SQL text) to determine cacheability:
Client → Query → Hyperdrive → Parse Protocol → Cacheable?
↓
Yes → Check Cache
↓
Hit? → Return Cached
↓
Miss → Query Database → Cache Result
Why wire protocol, not SQL parsing?
- More accurate (understands database's interpretation)
- Handles different SQL dialects
- Detects function volatility correctly
2. Cache Key Generation
Cache key includes:
- Query text
- Parameter values
- Database connection details
Example:
// These are DIFFERENT cache keys (different parameters)
await pool.query('SELECT * FROM users WHERE id = $1', [1]);
await pool.query('SELECT * FROM users WHERE id = $1', [2]);
// These are the SAME cache key (same query, same parameter)
await pool.query('SELECT * FROM users WHERE id = $1', [1]);
await pool.query('SELECT * FROM users WHERE id = $1', [1]); // Cache HIT
3. Cache Invalidation
Automatic Invalidation:
- Write queries invalidate related cached queries
- Hyperdrive tracks dependencies
- No manual invalidation needed
Example:
// 1. Query cached
await pool.query('SELECT * FROM users WHERE id = 1'); // Cache MISS, then cached
// 2. Read from cache
await pool.query('SELECT * FROM users WHERE id = 1'); // Cache HIT
// 3. Write invalidates cache
await pool.query('UPDATE users SET name = $1 WHERE id = 1', ['Jane']);
// 4. Query re-cached
await pool.query('SELECT * FROM users WHERE id = 1'); // Cache MISS (invalidated), then re-cached
Cache Configuration
Default Settings
Enabled by default - no configuration needed.
// Caching is automatically enabled
const client = new Client({
connectionString: env.HYPERDRIVE.connectionString
});
Disable Caching (Per Hyperdrive Config)
# Disable caching for specific Hyperdrive config
npx wrangler hyperdrive update <hyperdrive-id> --caching-disabled
# Re-enable caching
npx wrangler hyperdrive update <hyperdrive-id> --caching-disabled=false
When to disable:
- Debugging cache-related issues
- Testing without cache
- Workloads that never benefit from caching (all writes)
Optimizing for Caching
1. Use Prepared Statements (postgres.js)
Critical: postgres.js requires prepare: true for caching.
// ✅ Cacheable (prepared statements enabled)
const sql = postgres(env.HYPERDRIVE.connectionString, {
prepare: true // CRITICAL for caching
});
// ❌ NOT cacheable (prepare disabled)
const sql = postgres(env.HYPERDRIVE.connectionString, {
prepare: false // Queries won't be cached!
});
Why: Hyperdrive caches prepared statements, not simple queries from postgres.js.
2. Avoid Volatile Functions
Instead of:
-- ❌ Not cached (RANDOM)
SELECT * FROM products ORDER BY RANDOM() LIMIT 10;
Use:
// ✅ Cached (random offset generated in Worker)
const offset = Math.floor(Math.random() * 1000);
await pool.query('SELECT * FROM products OFFSET $1 LIMIT 10', [offset]);
3. Parameterize Queries
Instead of:
// ❌ Different cache keys for each user ID
await pool.query(`SELECT * FROM users WHERE id = ${userId}`);
Use:
// ✅ Same cache key structure, parameterized
await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
4. Keep Transactions Short
Avoid:
// ❌ Queries within transaction not cached
await client.query('BEGIN');
await client.query('SELECT * FROM users'); // Not cached
await client.query('SELECT * FROM posts'); // Not cached
await client.query('COMMIT');
Use:
// ✅ Queries outside transaction are cached
await client.query('SELECT * FROM users'); // Cached
await client.query('SELECT * FROM posts'); // Cached
5. Separate Read and Write Patterns
Organize code:
// Read-heavy endpoints (benefit from caching)
app.get('/users', async (c) => {
const users = await pool.query('SELECT * FROM users LIMIT 100');
return c.json(users.rows);
});
// Write-heavy endpoints (no caching)
app.post('/users', async (c) => {
const { name, email } = await c.req.json();
await pool.query('INSERT INTO users (name, email) VALUES ($1, $2)', [name, email]);
return c.json({ success: true });
});
Monitoring Cache Performance
Cache Status
Check cache hit/miss in response:
const response = await fetch('https://your-worker.dev/api/users');
const cacheStatus = response.headers.get('cf-cache-status');
console.log(cacheStatus);
// Values: HIT, MISS, BYPASS, EXPIRED, etc.
Cache Status Values:
HIT- Query result served from cacheMISS- Query not in cache, fetched from databaseBYPASS- Caching disabled or non-cacheable queryEXPIRED- Cached result expired, re-fetchedUNCACHEABLE- Query cannot be cached (write operation)
Hyperdrive Analytics
View cache metrics in Cloudflare dashboard:
- Go to Hyperdrive Dashboard
- Select your Hyperdrive configuration
- Click Metrics tab
Available Metrics:
- Cache hit ratio - % of queries served from cache
- Query count - Total queries
- Cache status breakdown - HIT vs MISS vs BYPASS
- Query latency - p50, p95, p99
- Result size - Bytes cached
Ideal Cache Hit Ratio:
- Read-heavy workload: 60-90% cache hit ratio
- Mixed workload: 30-60% cache hit ratio
- Write-heavy workload: 10-30% cache hit ratio
Cache Behavior by Query Type
| Query Type | PostgreSQL | MySQL | Cached? |
|---|---|---|---|
| SELECT | SELECT * FROM users |
SELECT * FROM users |
✅ Yes |
| INSERT | INSERT INTO users VALUES (...) |
INSERT INTO users VALUES (...) |
❌ No |
| UPDATE | UPDATE users SET name = ... |
UPDATE users SET name = ... |
❌ No |
| DELETE | DELETE FROM users WHERE ... |
DELETE FROM users WHERE ... |
❌ No |
| CREATE TABLE | CREATE TABLE new_table (...) |
CREATE TABLE new_table (...) |
❌ No |
| LASTVAL() | SELECT LASTVAL() |
SELECT LAST_INSERT_ID() |
❌ No |
| RANDOM() | SELECT ... ORDER BY RANDOM() |
SELECT ... ORDER BY RAND() |
❌ No |
| Transaction | BEGIN; SELECT ...; COMMIT; |
START TRANSACTION; SELECT ...; COMMIT; |
❌ No |
| JOIN | SELECT * FROM a JOIN b |
SELECT * FROM a JOIN b |
✅ Yes |
| Aggregate | SELECT COUNT(*) FROM users |
SELECT COUNT(*) FROM users |
✅ Yes |
| Subquery | SELECT * WHERE id IN (SELECT ...) |
SELECT * WHERE id IN (SELECT ...) |
✅ Yes |
Cache TTL
Default TTL: Hyperdrive manages TTL automatically based on workload patterns.
Cache invalidation:
- Write queries invalidate related cached queries
- TTL expires based on usage patterns
- No manual TTL configuration needed
Best Practices
- Enable prepared statements (postgres.js:
prepare: true) - Avoid volatile functions (RANDOM, LASTVAL, etc.)
- Use parameterized queries for consistent cache keys
- Keep transactions short (queries in transactions not cached)
- Separate read and write workloads for better cache efficiency
- Monitor cache hit ratio in Hyperdrive analytics
- Don't disable caching unless debugging (it's automatic and beneficial)
Troubleshooting
Low Cache Hit Ratio
Symptom: Most queries showing cache MISS.
Causes & Solutions:
1. Prepared statements disabled (postgres.js):
// ❌ Problem
const sql = postgres(url, { prepare: false });
// ✅ Solution
const sql = postgres(url, { prepare: true });
2. Using volatile functions:
-- ❌ Problem
SELECT LASTVAL(), * FROM articles;
-- ✅ Solution
SELECT * FROM articles;
3. Mostly write queries:
-- ❌ Problem (writes not cached)
INSERT INTO logs (...) VALUES (...);
-- ✅ Expected behavior (writes aren't cached, this is normal)
4. Transactions wrapping reads:
// ❌ Problem
await client.query('BEGIN');
await client.query('SELECT ...');
await client.query('COMMIT');
// ✅ Solution
await client.query('SELECT ...'); // No transaction
Queries Not Cached
Symptom: Expected cacheable queries showing BYPASS or UNCACHEABLE.
Debug Steps:
1. Check query is read-only:
-- ✅ Should be cached
SELECT * FROM users;
-- ❌ Not cached (write)
UPDATE users SET name = 'John';
2. Check for volatile functions:
-- ❌ Not cached
SELECT RANDOM();
-- ✅ Cached
SELECT * FROM products;
3. Check prepared statements enabled (postgres.js):
const sql = postgres(url, { prepare: true });
4. Check caching not disabled:
npx wrangler hyperdrive get <hyperdrive-id>
# Check "caching": { "disabled": false }