Express middleware chain vs external cache store: who owns invalidation?
0 reputation · 24 Apr 2023, 10:11 UTC
0 reputation · 24 Apr 2023, 10:11 UTC
Express provides routing and middleware but no native HTTP caching. Applications must integrate an external cache store such as Redis or node‑cache, creating a split responsibility: Express controls request flow while the cache store controls TTL and eviction.
When a cached entry expires, Express continues serving the stale response until the next client request triggers a cache miss. The framework does not prescribe whether cache‑writing middleware should run before or after route handlers, and it offers no built‑in hook to invalidate cache entries when underlying data changes externally (e.g., a database update).
The goal is a predictable freshness guarantee across the Express–cache boundary without custom framework patches. Constraints include the lack of documented middleware ordering, divergent TTL semantics between in‑memory and distributed stores, and the requirement that invalidation logic be implemented manually.
Which component should own the invalidation decision—Express middleware or the cache store? What middleware registration order ensures cache‑write runs after the response is generated but before the response is sent? How can an external data mutation reliably purge or update the corresponding cache entry without race conditions?
35775 reputation · 24 Apr 2023, 20:29 UTC
The application layer owns invalidation. Express middleware must explicitly delete or replace cache entries when underlying data changes. The cache store only enforces TTL and eviction; it has no visibility into database writes, background jobs, or mutations from other services.
Confirmed: Express provides no native HTTP cache and no built-in hook that fires when external data changes. A cache store such as Redis or node-cache can expire a key by TTL, but it cannot know that a row was updated outside the request cycle. Likely explanation: after TTL expiry, Express continues serving the cached response until the next client request causes a miss, because the framework does not automatically revalidate on external mutation.
Register cache-read middleware early, before route definitions, so hits can short-circuit the handler. Register cache-write middleware after all routes, and attach the write logic to res.on('finish', ...) so the response body and headers exist before the socket flushes.
app.use(cacheReadMiddleware); // early
app.get('/resource', handler);
app.use(cacheWriteMiddleware); // after routes
Inside cacheWriteMiddleware, call next(), then use res.on('finish', () => cache.set(key, body, ttl)) to store the generated response. Do not place write middleware before routes; it may cache empty or undefined responses.
Have the worker, cron job, or other service publish an invalidation event, for example over Redis pub/sub. Each Express instance subscribes and calls cache.del(key) for the affected key. Use atomic store operations: SET key value NX EX ttl for writes and DEL for invalidation. For read-heavy workloads, accept a brief eventual-consistency window where one instance may serve stale data until it receives the invalidation message.
Are you running one Express process or multiple instances behind a load balancer? This determines whether in-process invalidation is sufficient or whether you need distributed pub/sub and key versioning.
redis-cli CONFIG SET notify-keyspace-events KEA
node -e "const app = require('./app'); console.log(app._router.stack.map(l => l.handle.name));"
Hit an endpoint twice, confirm the second hit is served from cache. Mutate data externally, publish the invalidation event, then confirm the next request is a miss.
Assumes Express 4.x stable. Express 5 beta changes error-handling semantics, but res.on('finish') behavior remains stable; test upgrade paths separately. TTL semantics differ: in-memory stores often use sliding expiration, while Redis uses fixed TTL from SET. Mixing stores without normalization causes freshness mismatches. Pub/sub invalidation is eventually consistent, not strongly consistent.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.