Running an eCommerce store with one million products creates a very different performance challenge from running a small catalog.
Pages need to load quickly, product data needs to stay reasonably fresh, and the backend must handle traffic without processing the same requests repeatedly.
While building a headless Bagisto storefront with Next.js, we needed a caching strategy that could scale with a large catalog without making deployments slower or putting unnecessary load on the backend.
The solution was not a single caching technique. We combined request-level caching, cache tags, on-demand page generation, a small in-memory cache, and webhook-based revalidation.
If you’re building a headless storefront, our guide on How to Build eCommerce in Next.js covers the foundation.
The Challenge of a One-Million-Product Catalog
A large product catalog creates two problems that need to be solved together: performance and freshness.
Pre-building every product page during deployment is not practical when the catalog contains one million products. Build times can increase significantly, and deployments become harder to manage.
Fetching product data from the backend on every request creates a different problem. The backend has to process repeated requests for the same data, increasing server load and potentially increasing response times.
Caching everything indefinitely is not the answer either. Product prices, inventory, availability, and content can change.
The real challenge is finding the right balance between speed and freshness.

Why We Stopped Treating Cache as One Thing
Not all ecommerce data changes at the same frequency. Store configurations may remain unchanged for days.
Product information can change a few times throughout the day. Cart and checkout data can change every second.
Because of this, we moved away from a single caching strategy and started caching data based on how often it changes.
This simple decision became the foundation of our entire optimization approach.
One Entry Point for Every GraphQL Request
To keep caching consistent, every server-side GraphQL request goes through a single helper function.
This helper wraps Next.js caching APIs and applies cache tags, cache keys, and cache lifetimes automatically.
graphqlRequest(
GET_PRODUCT_BY_URL_KEY,
{ urlKey },
{ tags: ["products", `product-${urlKey}`],
life: "hours",
} );
This approach ensures every request follows the same caching rules.
It also gives us one place to update caching behavior when requirements change.
Why Cache Tags Matter
Cache tags allow us to invalidate specific content when data changes. Instead of clearing the entire cache, we can refresh only the affected product.
This keeps cache invalidation fast and efficient.
Building Stable Cache Keys
A cache is only effective when identical requests generate identical keys. We generate cache keys using the GraphQL query and its variables in a stable, sorted format. As a result, duplicate requests always hit the same cache entry.
For large catalogs, this significantly reduces unnecessary backend traffic.
Match Cache Lifetime to the Data
We assign cache durations based on how frequently data changes.
// Human-friendly cache lifetimes seconds → 10 s minutes → 60 s hours → 3600 s days → 86400 s
Here is how we assign them across the catalog.
| Data | Lifetime |
|---|---|
| Store config, menus | days |
| Product pages | hours |
| Category listings | minutes |
| Search and filters | seconds |
| Cart, customer | never cached |
The rule is easy to remember. Cache for as long as the data stays true.
Generating Pages On Demand
The biggest optimization was avoiding the need to pre-build a million pages. We only generate a small set of popular products during deployment.
All other product pages are generated when they receive their first visit.
export const dynamic = "force-static"; export const revalidate = 86400; export const dynamicParams = true;
This keeps build times fast regardless of catalog size.
Once generated, pages are cached and served like static content.
Using a Small Memory Cache
The same product can be requested multiple times during a render cycle. To avoid duplicate requests, we use a lightweight LRU cache.
const productCache = new LRUCache<ProductNode>(100, 10);
This reduces unnecessary backend calls while keeping memory usage predictable.
Keeping Cached Content Fresh
Long cache times are only safe if you can clear them on demand.
Bagisto sends a webhook whenever a product or collection changes.
if ( isProductUpdate || isCollectionUpdate )
{
revalidatePath("/", "layout");
}
This allows product updates to appear quickly without waiting for cache expiration.
Final Thoughts
Supporting one million products in Next.js does not mean building one million pages during every deployment or sending every request directly to the backend.
Centralized caching, on-demand page generation, cache tagging, webhook revalidation, and strict handling of user data helped us keep the storefront fast while maintaining fresh content.
For large headless ecommerce implementations, these patterns provide a practical way to scale Next.js without sacrificing performance or maintainability.
Be the first to comment.