Skip to main content
Version: 2.75.2

How to Setup HTTP Caching with Watt

Problem​

You need to improve your application's performance by caching HTTP responses, but you want:

  • Intelligent cache invalidation when data changes
  • Fine-grained control over what gets cached and for how long
  • Ability to invalidate cache by specific tags or routes
  • Built-in caching without external dependencies like Redis

When to use this solution:

  • Your API serves data that doesn't change frequently
  • You want to reduce database load and improve response times
  • You need cache invalidation when specific resources are updated
  • You're building high-traffic applications that need performance optimization

Solution Overview​

Watt provides built-in HTTP caching with tag-based invalidation. This guide shows you how to:

  1. Enable HTTP caching in your Watt application
  2. Tag responses for intelligent cache invalidation
  3. Invalidate cache by specific routes or tags
  4. Test your caching implementation

Prerequisites​

Before starting, ensure you have:

  • Node.js (v20.16.0+ or v22.3.0+)
  • npm (v10 or higher)
  • A Watt application (setup guide)
  • Basic understanding of HTTP caching headers

Step 1: Enable HTTP Cache in Watt​

Add HTTP caching configuration to your watt.json file:

{
"$schema": "https://schemas.platformatic.dev/@platformatic/runtime/2.17.0.json",
"httpCache": {
"cacheTagsHeader": "X-Cache-Tags"
},
"services": [
// your services here
]
}

What this does:

  • Enables Watt's built-in HTTP caching layer
  • Sets up cache tag header for intelligent invalidation
  • No external cache services needed (Redis, Memcached, etc.)

Step 2: Add Cache Headers to Your Responses​

In your service endpoints, add appropriate cache headers:

// routes/products.js
module.exports = async function (fastify) {
fastify.get('/cached-counter', async (req, reply) => {
// Set cache duration (10 minutes)
reply.header('Cache-Control', 'public, s-maxage=600')

// Tag this response for invalidation
reply.header('X-Cache-Tags', 'counter-data')

const counter = await getCounterFromDatabase()
return { counter, timestamp: new Date().toISOString() }
})
}

Cache header explanation:

  • Cache-Control: public, s-maxage=600 - Cache for 600 seconds (10 minutes)
  • X-Cache-Tags: counter-data - Tag for targeted invalidation

Understanding Cache Tags​

Cache tags are unique identifiers that let you invalidate related cache entries:

  • Resource-based tags: product-42, user-123, order-456
  • Category-based tags: products, users, orders
  • UUID tags: 550e8400-e29b-41d4-a716-446655440000

Best practices:

  • Use descriptive, consistent tag names
  • Tag by both specific resource and category
  • Consider using UUIDs for guaranteed uniqueness

Step 3: Implement Cache Invalidation​

Method 1: Invalidate by Specific Route​

When you need to invalidate cache for a specific endpoint:

// routes/admin.js  
module.exports = async function (fastify) {
fastify.delete('/invalidate-counter-cache', async (req, reply) => {
await globalThis.platformatic.invalidateHttpCache({
keys: [
{
origin: 'http://internal.plt.local',
path: '/cached-counter',
method: 'GET'
}
]
})

return { message: 'Cache invalidated for counter endpoint' }
})
}

Method 2: Invalidate by Cache Tags​

For more flexible invalidation across multiple related endpoints:

// routes/admin.js
module.exports = async function (fastify) {
fastify.delete('/invalidate-by-tags', {
schema: {
querystring: {
type: 'object',
properties: {
tags: { type: 'string' }
},
required: ['tags']
}
}
}, async (req, reply) => {
const tags = req.query.tags.split(',')

await globalThis.platformatic.invalidateHttpCache({ tags })

return {
message: `Cache invalidated for tags: ${tags.join(', ')}`,
invalidatedTags: tags
}
})
}

Automatic Invalidation on Data Updates​

Invalidate cache automatically when data changes:

// routes/products.js
module.exports = async function (fastify) {
fastify.post('/products', async (req, reply) => {
const newProduct = await createProduct(req.body)

// Invalidate all product-related cache
await globalThis.platformatic.invalidateHttpCache({
tags: ['products', `product-${newProduct.id}`]
})

return newProduct
})

fastify.put('/products/:id', async (req, reply) => {
const updatedProduct = await updateProduct(req.params.id, req.body)

// Invalidate specific product cache
await globalThis.platformatic.invalidateHttpCache({
tags: [`product-${req.params.id}`, 'products']
})

return updatedProduct
})
}

Step 4: Verification and Testing​

Test Cache Behavior​

1. Start your Watt application:

npm run dev

2. Test cached responses:

# First request - cache miss (hits database)
curl -i http://localhost:3042/cached-counter

# Second request - cache hit (returns cached data)
curl -i http://localhost:3042/cached-counter

What to verify:

  • First response includes X-Cache-Tags: counter-data
  • Subsequent requests return identical data (counter doesn't increment)
  • Response includes proper Cache-Control headers

3. Test cache invalidation:

# Invalidate by specific route
curl -X DELETE http://localhost:3042/invalidate-counter-cache

# Or invalidate by tags
curl -X DELETE "http://localhost:3042/invalidate-by-tags?tags=counter-data"

# Verify cache was invalidated
curl -i http://localhost:3042/cached-counter

Expected behavior:

  • After invalidation, counter value should increment on next request
  • Response should have fresh timestamp

Complete Example Implementation​

Here's a complete working example that demonstrates all caching concepts:

// routes/cache-demo.js
module.exports = async function (fastify) {
let counter = 0

// Cached endpoint
fastify.get('/cached-counter', {
schema: {
response: {
200: {
type: 'object',
properties: {
counter: { type: 'number' },
timestamp: { type: 'string' },
cached: { type: 'boolean' }
}
}
}
}
}, async (req, reply) => {
counter++

// Set caching headers
reply.header('Cache-Control', 'public, s-maxage=600') // 10 minutes
reply.header('X-Cache-Tags', 'counter-data,demo-data')

return {
counter,
timestamp: new Date().toISOString(),
cached: true
}
})

// Invalidation endpoints
fastify.delete('/invalidate-counter', async (req, reply) => {
await globalThis.platformatic.invalidateHttpCache({
keys: [{
origin: 'http://localhost:3042',
path: '/cached-counter',
method: 'GET'
}]
})

return { message: 'Counter cache invalidated' }
})

fastify.delete('/invalidate-tags', {
schema: {
querystring: {
type: 'object',
properties: {
tags: { type: 'string' }
},
required: ['tags']
}
}
}, async (req, reply) => {
const tags = req.query.tags.split(',')
await globalThis.platformatic.invalidateHttpCache({ tags })

return { message: `Invalidated tags: ${tags.join(', ')}` }
})
}

Troubleshooting​

Cache Not Working​

Problem: Responses aren't being cached

Solutions:

  • Verify httpCache is enabled in watt.json
  • Check that Cache-Control headers are set correctly
  • Ensure cache duration is greater than 0 (s-maxage=600)
  • Confirm Watt version supports HTTP caching

Cache Not Invalidating​

Problem: Cache entries persist after invalidation

Solutions:

  • Check that X-Cache-Tags header matches invalidation tags exactly
  • Verify the cacheTagsHeader configuration in watt.json
  • Ensure invalidation code runs without errors
  • Confirm route/method/origin match exactly for key-based invalidation

Performance Issues​

Problem: Caching isn't improving performance

Solutions:

  • Profile your application to identify bottlenecks
  • Ensure cached endpoints are actually being called frequently
  • Consider cache duration - too short defeats the purpose
  • Monitor cache hit rates vs. miss rates

Memory Usage​

Problem: High memory usage with caching enabled

Solutions:

  • Set appropriate cache TTL values
  • Limit cacheable response sizes
  • Monitor cache size and implement eviction policies if needed
  • Consider external caching solutions for very large datasets

Next Steps​

Now that you have HTTP caching working: