
Ecommerce Caching
- 63 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Add layered caching (CDN edge, Redis, full-page) with cart-aware invalidation to speed up storefront pages.
About
Covers multi-layer commerce caching and the challenges of personalized content, changing inventory, and instant purge on price changes. A developer uses it when product pages are slow or preparing TTFB for high-traffic sale events.
- Per-platform table of what caching is managed vs what you control
- Cart-aware invalidation reacting to product, price, and inventory changes
Ecommerce Caching by the numbers
- 63 all-time installs (skills.sh)
- Ranked #680 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill ecommerce-cachingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 63 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Add layered caching (CDN edge, Redis, full-page) with cart-aware invalidation to speed up storefront pages.
Files
E-commerce Caching
Overview
Implement multi-layer caching for e-commerce applications covering CDN edge caching, application-level caching (Redis), and full-page caching with cart-aware invalidation. This skill addresses the unique challenges of caching commerce pages — personalized content (cart count, logged-in state), frequently changing inventory and prices, and the need for instant cache purging when products or prices change.
When to Use This Skill
- When product and collection pages are slow due to database queries and API calls
- When implementing a CDN or edge caching strategy for a storefront
- When adding Redis caching for product data, inventory, and session management
- When building cache invalidation logic that reacts to product, price, or inventory changes
- When optimizing time-to-first-byte (TTFB) for high-traffic sale events
Core Instructions
Step 1: Determine your platform and what you can control
| Platform | What's Managed For You | What You Control |
|---|---|---|
| Shopify | CDN, full-page caching, server infrastructure | Liquid template efficiency, image optimization, app performance, HTTP cache headers on custom routes |
| WooCommerce | Nothing (you own the server) | Everything: page cache, object cache, CDN, database queries |
| BigCommerce | CDN, full-page caching, server infrastructure | Theme performance, image optimization, app overhead |
| Custom / Headless | Nothing (you own the server) | Everything: CDN configuration, Redis, Varnish, application cache, invalidation |
Step 2: Platform-specific caching configuration
---
Shopify
Shopify handles CDN and full-page caching automatically. Your optimization levers are:
Theme performance (Liquid): 1. Reduce the number of Liquid {% render %} calls in your product and collection templates — each render tag adds rendering time 2. In Online Store → Themes → Edit code, avoid loops that query the catalog repeatedly; use {% liquid %} blocks for conditional logic 3. Enable Shopify's Lazy loading for product images in Theme settings — this prevents below-fold images from blocking page load
App performance: 1. Audit your installed apps in Apps → App usage or with Shopify's Storefront Performance dashboard (Online Store → Themes → View report) 2. Apps that inject scripts into every page (especially checkout) can add 200–500ms per page load 3. Remove unused apps — even inactive apps with installed scripts slow down your store 4. For apps that add storefront JavaScript, check if they support Delayed loading (loads after page interactive) in their settings
CDN image optimization: Shopify automatically serves images via its CDN with WebP conversion and responsive sizing. To ensure you're using this: 1. Always use Shopify's Liquid img_url filter to generate image URLs — this routes through the Shopify CDN 2. Add width and height attributes to all <img> tags in your Liquid to prevent Cumulative Layout Shift 3. In Online Store → Themes → Customize → Theme settings → Images, verify lazy loading is enabled
---
WooCommerce
WooCommerce runs on your hosting infrastructure. Implement a three-layer caching stack:
Layer 1: PHP Object Cache (Redis) 1. Your hosting must support Redis (WP Engine, Kinsta, Cloudways, and most managed WordPress hosts do — check your control panel) 2. Install Redis Object Cache (free, wordpress.org) 3. Go to Settings → Redis and click Enable Object Cache 4. This caches all WordPress and WooCommerce database queries in Redis; most stores see 60–80% reduction in database load
Layer 2: Page Cache 1. Install WP Rocket ($59/yr — the most effective WordPress page cache plugin) or LiteSpeed Cache (free, if your host uses LiteSpeed) 2. In WP Rocket: go to Cache → Enable caching for logged-in WordPress users — keep this off; cached pages should only serve anonymous visitors 3. Enable Preload cache: WP Rocket will crawl and pre-cache your product and shop pages 4. Enable Separate cache file for mobile if your mobile and desktop layouts differ significantly
Layer 3: CDN 1. Configure Cloudflare (free tier available) as your CDN:
- Add your domain to Cloudflare and point DNS to Cloudflare's nameservers
- Install the Cloudflare WordPress plugin to enable cache purging from wp-admin
- Set Caching level to Standard in Cloudflare dashboard
- In WP Rocket, enable Cloudflare integration in the CDN settings — this allows WP Rocket to purge Cloudflare cache when you update products
2. Or use BunnyCDN ($1/mo for bandwidth) which integrates with WP Rocket natively
Cache invalidation for WooCommerce: WP Rocket and LiteSpeed Cache automatically purge cached pages when products are updated via the wp-admin. For programmatic updates, add this to your child theme's functions.php:
// Purge product page cache when inventory or price changes
add_action('woocommerce_product_set_stock', function($product) {
if (function_exists('rocket_clean_post')) {
rocket_clean_post($product->get_id()); // WP Rocket
}
if (class_exists('LiteSpeed_Cache_API')) {
LiteSpeed_Cache_API::purge_post($product->get_id()); // LiteSpeed
}
});---
Custom / Headless
Cache layers for a headless Node.js storefront:
Browser → CDN (Cloudflare/Fastly) → Application server → Redis → Database
[Static assets: 1yr] [Product data: 5min] [Catalog queries]
[Product pages: 5min]
[Cart: never]HTTP cache headers by content type:
// middleware/cacheHeaders.js
export function setCacheHeaders(req, res, next) {
const path = req.path;
// Cart, checkout, account — never cache (personalized)
if (path.startsWith('/cart') || path.startsWith('/checkout') || path.startsWith('/account')) {
res.setHeader('Cache-Control', 'private, no-store');
return next();
}
// Product pages — 5 min at CDN, serve stale while revalidating
if (path.match(/^\/products\/[\w-]+$/)) {
res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=300, stale-while-revalidate=600');
res.setHeader('Surrogate-Key', `product product-${path.split('/').pop()}`);
return next();
}
// Collection pages
if (path.match(/^\/collections\/[\w-]+$/)) {
res.setHeader('Cache-Control', 'public, max-age=120, s-maxage=600, stale-while-revalidate=1200');
res.setHeader('Surrogate-Key', `collection collection-${path.split('/').pop()}`);
return next();
}
// Static assets with content hash in filename — immutable for 1 year
if (path.match(/\.(js|css|png|jpg|webp|woff2|svg)(\?|$)/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
return next();
}
next();
}Redis product cache (cache-aside pattern):
// lib/productCache.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export async function getCachedProduct(productId, fetchFn) {
const key = `product:${productId}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const product = await fetchFn();
// Non-blocking write — serve response immediately
redis.setex(key, 300, JSON.stringify(product)).catch(() => {});
return product;
}
export async function invalidateProduct(productId, productSlug, collectionIds = []) {
const keys = [
`product:${productId}`,
`product:slug:${productSlug}`,
...collectionIds.map(id => `collection:${id}`),
];
await redis.del(...keys);
}Personalized content (cart count, inventory) loaded client-side after cached page:
<!-- Serve the cached page with placeholder for personalized data -->
<span id="cart-count" data-hydrate="true">0</span>
<script>
// Load personalized data after the cached page renders
fetch('/api/session-data', { credentials: 'include' })
.then(r => r.json())
.then(data => {
document.getElementById('cart-count').textContent = data.cartCount;
});
</script>// GET /api/session-data — NOT cached, reads from Redis
export async function sessionData(req, res) {
res.setHeader('Cache-Control', 'private, no-store');
const sessionId = req.cookies.session_id;
const cartCount = sessionId ? await redis.get(`cart:count:${sessionId}`) : '0';
res.json({ cartCount: parseInt(cartCount ?? '0') });
}Event-driven cache invalidation:
// React to product/price changes from your webhook or admin
export async function onProductUpdated({ productId, productSlug, collectionIds }) {
// 1. Invalidate Redis cache
await invalidateProduct(productId, productSlug, collectionIds);
// 2. Purge CDN (Cloudflare example)
await fetch(`https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${CF_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ tags: [`product-${productSlug}`] }), // tag-based purge (Cloudflare Enterprise)
// For free/Pro Cloudflare, use files: [`https://store.com/products/${productSlug}`]
});
}Best Practices
- Use `stale-while-revalidate` — serve cached content instantly while fetching a fresh version in the background; this eliminates cache miss latency for users
- Cache the page, hydrate personalization client-side — cache full HTML for anonymous visitors and load cart count / inventory via a fast
no-storeAPI call - Invalidate on stock-status change, not every inventory update — a quantity change from 50 to 49 doesn't need a purge; 1 to 0 (out-of-stock) does
- Strip tracking parameters for better CDN hit rates —
?utm_source=email&utm_campaign=springcreates unique cache entries; strip these at the CDN or in your URL normalization - Monitor cache hit rate — target 90%+ at CDN; use
X-Cacheresponse headers to identify frequent cache misses
Common Pitfalls
| Problem | Solution |
|---|---|
| Cart count shows 0 on cached pages | Never embed cart count in cached HTML; load it client-side via a fast no-store API endpoint after page load |
| Product price updated but CDN shows old price | Implement event-driven invalidation that purges CDN on price change; don't rely on TTL expiration alone |
| Thundering herd when cache expires | Use stale-while-revalidate so only one request revalidates while all others get slightly stale content |
| Low CDN hit rate | Normalize URLs by stripping tracking params; minimize Vary headers (e.g., Vary: Cookie effectively disables caching) |
| Redis memory grows unbounded | Set maxmemory and maxmemory-policy allkeys-lru in Redis config; monitor eviction rate |
Related Skills
- @flash-sale-scaling
- @edge-commerce
- @database-optimization-commerce
- @monitoring-alerting-commerce
{
"context": "Tests whether the agent correctly applies cache-control headers with appropriate TTLs per page type, includes stale-while-revalidate, never caches cart/checkout/account pages, sets Surrogate-Key headers for targeted CDN purging, adds X-Cache debug headers via Varnish, strips marketing query parameters, removes cookies for cacheable pages in Varnish, and avoids Vary: Cookie.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Product page s-maxage",
"max_score": 7,
"description": "Product pages have s-maxage=300 (5 minutes) in their Cache-Control header"
},
{
"name": "Product page max-age",
"max_score": 7,
"description": "Product pages have max-age=60 (1 minute) in their Cache-Control header"
},
{
"name": "Collection page s-maxage",
"max_score": 7,
"description": "Collection pages have s-maxage=600 (10 minutes) in their Cache-Control header"
},
{
"name": "Static assets 1-year TTL",
"max_score": 7,
"description": "Static assets (CSS/JS/images) use max-age=31536000 and the 'immutable' directive"
},
{
"name": "stale-while-revalidate on product pages",
"max_score": 8,
"description": "Product pages include stale-while-revalidate in their Cache-Control header"
},
{
"name": "Cart/checkout never cached",
"max_score": 8,
"description": "Cart, checkout, and account paths use Cache-Control: private, no-store (or equivalent) and are NOT given any public caching directive"
},
{
"name": "Surrogate-Control no-store for cart",
"max_score": 7,
"description": "Cart/checkout/account responses include a Surrogate-Control: no-store header"
},
{
"name": "Surrogate-Key for products",
"max_score": 8,
"description": "Product page responses include a Surrogate-Key header containing 'product' and 'product-{slug}' (or equivalent per-product key)"
},
{
"name": "Surrogate-Key for collections",
"max_score": 8,
"description": "Collection page responses include a Surrogate-Key header containing 'collection' and 'collection-{slug}'"
},
{
"name": "X-Cache HIT/MISS header",
"max_score": 7,
"description": "Varnish VCL sets an X-Cache response header to 'HIT' when obj.hits > 0 and 'MISS' otherwise"
},
{
"name": "Strip tracking params",
"max_score": 8,
"description": "Marketing/tracking query parameters (utm_, fbclid, gclid, mc_, or similar) are stripped from URLs before caching (in Varnish VCL or middleware)"
},
{
"name": "Unset Cookie for cacheable pages",
"max_score": 8,
"description": "Varnish VCL removes/unsets request cookies for product, collection, and home pages before hashing (to prevent Vary: Cookie from disabling caching)"
},
{
"name": "Varnish grace period",
"max_score": 10,
"description": "Varnish VCL sets beresp.grace to at least double the TTL for product and/or collection pages"
}
]
}
E-commerce CDN and Reverse Proxy Caching Configuration
Problem/Feature Description
Prism Goods is a fast-growing direct-to-consumer brand that recently moved its storefront to a Node.js application behind Varnish and a CDN. Page load times are acceptable in testing but the team is seeing poor cache hit rates in production (around 30%) because cache headers are inconsistent across page types, tracking parameters from ad campaigns are generating thousands of unique cache keys, and the Varnish configuration is not stripping session cookies from cacheable pages.
The team wants a complete, consistent caching configuration that covers their Node.js middleware (for setting HTTP cache headers on every response type) and their Varnish VCL (for full-page caching at the reverse proxy). They need different caching rules for product listing pages, individual product pages, static assets, and cart/checkout flows. The configuration must also integrate with their CDN's targeted purging feature so that individual products can be purged without flushing everything. An important constraint is that their marketing team appends UTM and other tracking parameters to all URLs in ad campaigns, so the caching layer must handle these without fragmenting the cache.
Output Specification
Produce the following files:
middleware/cache-headers.ts— Express/Node.js middleware that sets appropriateCache-Control(and related) headers for each type of request pathvarnish.vcl— A Varnish 4.1 VCL configuration withvcl_recv,vcl_backend_response, andvcl_deliversubroutines covering product pages, collection pages, static assets, cart/checkout, and homepagedocs/caching-strategy.md— A short document (bullet points are fine) summarising the TTL decisions made for each page type and explaining the CDN purging approach
The output should be complete enough that a developer can review the strategy and the operations team can deploy it.
{
"context": "Tests whether the agent builds event-driven cache invalidation (not TTL-only), correctly purges collections on price changes, only invalidates page cache when stock status transitions (not on every quantity update), stores inventory in Redis with the correct TTL, uses client-side hydration for personalized content rather than embedding it in cached HTML, implements the personalization endpoint with no-store headers and parallel Redis reads, and uses a PostgreSQL materialized view with CONCURRENTLY refresh for collection listing data.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Event-driven invalidation",
"max_score": 8,
"description": "Cache invalidation is triggered by named events (product updated, price changed, inventory changed) rather than relying solely on TTL expiry"
},
{
"name": "Price change purges collections",
"max_score": 9,
"description": "When a price changes, cache invalidation purges BOTH the product cache AND the related collection page caches (not just the product)"
},
{
"name": "Inventory stock-status gating",
"max_score": 10,
"description": "On inventory quantity change, page cache purge only fires when the stock STATUS changes (in-stock to out-of-stock or vice versa) — a quantity change between two non-zero values does NOT trigger a cache purge"
},
{
"name": "Inventory stored in Redis",
"max_score": 8,
"description": "Inventory quantity is written to Redis using a key pattern like `inventory:{productId}` (or similar) after inventory change events"
},
{
"name": "Inventory Redis TTL is 1 hour",
"max_score": 8,
"description": "The Redis inventory key is set with a TTL of 3600 seconds (1 hour)"
},
{
"name": "Personalized content not in cached HTML",
"max_score": 9,
"description": "The server-rendered product page HTML does NOT embed real cart count or live inventory data directly — instead it uses placeholders or a loading state for those elements"
},
{
"name": "Client-side hydration script",
"max_score": 8,
"description": "The rendered product page includes a client-side script that fetches personalization data (cart count, inventory) from a separate API endpoint after page load"
},
{
"name": "Personalization API no-store",
"max_score": 9,
"description": "The personalization API endpoint sets Cache-Control: private, no-store (preventing it from being cached at CDN or browser)"
},
{
"name": "Parallel Redis reads in personalization API",
"max_score": 8,
"description": "The personalization API fetches cart count and inventory from Redis using Promise.all (or equivalent parallel execution) rather than sequentially"
},
{
"name": "Materialized view for collection listings",
"max_score": 8,
"description": "A PostgreSQL materialized view is defined (CREATE MATERIALIZED VIEW) for collection product listing data rather than a regular table or live query"
},
{
"name": "Unique index on materialized view",
"max_score": 8,
"description": "A unique index is created on the materialized view (required for CONCURRENTLY refresh)"
},
{
"name": "CONCURRENTLY refresh",
"max_score": 7,
"description": "The materialized view is refreshed using REFRESH MATERIALIZED VIEW CONCURRENTLY (not a plain REFRESH without CONCURRENTLY)"
}
]
}
Cache Invalidation System for a Live Commerce Platform
Problem/Feature Description
Vanta Shop runs a high-traffic online store with thousands of products. Their marketing team regularly updates prices for promotions, and the warehouse team adjusts inventory levels throughout the day. The current setup caches all product and collection pages at the CDN, but after price changes or inventory updates the site sometimes shows stale prices or incorrect stock availability for up to 10 minutes — until the cached pages expire naturally. During a recent sale, several customers checked out at the wrong price because the cache hadn't expired yet.
The engineering team needs a cache invalidation system that reacts to catalog events in real time. When a product's price changes, the right pages must be cleared immediately. Inventory changes are trickier: every warehouse adjustment should not flush the cache (there can be hundreds per hour), but customers must see accurate in/out-of-stock status. The team also has a requirement that product pages must be cacheable at the CDN edge for anonymous users, yet each visitor still sees their own live cart count and current inventory status — so the personalization approach must not break CDN caching.
Additionally, the collection listing pages are generated from a complex JOIN across several tables and the DBA wants to move that query to a pre-computed structure in PostgreSQL that can be updated without blocking concurrent reads.
Output Specification
Produce the following files:
src/cache-invalidator.ts— A TypeScriptCacheInvalidatorclass with handlers for product update, price change, inventory change, and bulk catalog update events. Include a brief JSDoc comment on each method explaining when it fires and what it does.src/personalization-api.ts— An Express route handler for a/api/personalizationendpoint that returns cart count and inventory for a given product, with appropriate cache headerssrc/product-page-renderer.ts— A function that renders a product page HTML string. The rendered page should include placeholders for cart count and inventory status that are populated by a client-side script calling the personalization APIdb/collection-listing.sql— SQL statements to create and maintain the collection product listing data structure in PostgreSQL, along with the necessary indexpackage.json— With the required dependencies listed
The code does not need to run end-to-end (stubs for external services like CDN purge APIs are fine), but the logic, structure, and configuration choices must be complete and correct.
{
"context": "Tests whether the agent uses ioredis for Redis caching, configures the client with correct resilience settings, applies proper key naming conventions, uses fire-and-forget cache writes, and leverages Redis pipelines for bulk operations.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Uses ioredis package",
"max_score": 10,
"description": "Imports from 'ioredis' (not 'redis', 'node-redis', or another Redis client library)"
},
{
"name": "maxRetriesPerRequest setting",
"max_score": 8,
"description": "Redis client is constructed with maxRetriesPerRequest: 3"
},
{
"name": "enableReadyCheck setting",
"max_score": 8,
"description": "Redis client is constructed with enableReadyCheck: true"
},
{
"name": "Retry strategy with cap",
"max_score": 8,
"description": "retryStrategy function uses Math.min with a 2000ms cap (e.g., Math.min(times * 50, 2000))"
},
{
"name": "Key prefix pattern",
"max_score": 8,
"description": "Product cache keys use a consistent prefix pattern (e.g., 'product:' followed by the product ID)"
},
{
"name": "Slug index key",
"max_score": 8,
"description": "Products are also indexed by slug using a separate Redis key (e.g., 'product:slug:{slug}' or similar) that stores the product ID"
},
{
"name": "Fire-and-forget cache write",
"max_score": 10,
"description": "In the getOrFetch / cache-aside method, the cache write is NOT awaited — it is fired with .catch() for error handling rather than awaited before returning the response"
},
{
"name": "Pipeline for bulk operations",
"max_score": 10,
"description": "When warming the cache for multiple products, redis.pipeline() is used and a single pipeline.exec() call is made rather than individual setex calls per product"
},
{
"name": "setex for TTL",
"max_score": 8,
"description": "Cache entries are stored with setex (or set with EX option) rather than plain set with no expiry"
},
{
"name": "Slug key cleanup on invalidation",
"max_score": 8,
"description": "When invalidating a product, both the product ID key AND the slug key are deleted together"
},
{
"name": "Product TTL value",
"max_score": 7,
"description": "Product cache TTL is set to 300 seconds (5 minutes) by default"
},
{
"name": "maxmemory-policy mentioned",
"max_score": 7,
"description": "Redis configuration or comments include mention of maxmemory and/or maxmemory-policy allkeys-lru to prevent unbounded memory growth"
}
]
}
Product Caching Service
Problem/Feature Description
Meridian Commerce is a mid-size online retailer whose Node.js backend makes heavy database calls on every product page request — fetching product data, variants, and images on each hit. During a recent flash sale, response times spiked to over 3 seconds and the site nearly went down under load. The engineering team wants to introduce an application-level caching layer that sits between the application and the database, so that frequently accessed product data is served from memory rather than hitting the database on every request.
The team uses TypeScript throughout their stack and already has a Redis instance available. They need a reusable ProductCache class that handles get, set, invalidation, and bulk cache warming — complete with sensible resilience settings for production. Performance is critical: cache writes must never block response delivery, and warming hundreds of products at once must be done as efficiently as possible. A separate scripts/warm-cache.ts script should also be provided that demonstrates warming the cache for a given collection.
Output Specification
Produce the following files:
src/product-cache.ts— A TypeScriptProductCacheclass with methods for:- Getting a product by ID
- Getting a product ID by slug
- Setting / caching a product (with TTL)
- Fetching with cache-aside logic (cache miss triggers fetch, result is cached)
- Invalidating a product (removing all keys related to it)
- Bulk warming the cache for a collection of products
src/redis-client.ts— A module that creates and exports a configured Redis client instancescripts/warm-cache.ts— A standalone script that demonstrates warming the cache for a collection of products (can use mock/stub data)package.json— With the required dependencies listed
Input Files
The following type definitions are provided as inputs. Extract them before beginning.
=============== FILE: src/types.ts =============== export interface ProductVariant { id: string; title: string; price: number; inventoryQuantity: number; }
export interface Product { id: string; title: string; slug: string; status: 'active' | 'draft' | 'archived'; collectionIds: string[]; variants: ProductVariant[]; updatedAt: string; }
{
"name": "finsi/ecommerce-caching",
"version": "0.1.0",
"summary": "Multi-layer caching — CDN, application, database, and cart-aware cache invalidation",
"skills": {
"ecommerce-caching": {
"path": "SKILL.md"
}
}
}