A Medusa.js store that works beautifully at a hundred orders a day will not automatically hold up when that number climbs to ten thousand. Performance at scale is not an afterthought you can bolt on later. It is a set of architectural decisions that need to be woven into your infrastructure from the moment you start thinking about growth.
This guide covers the three most impactful layers of performance optimisation for Medusa.js stores: CDN configuration, Redis-based caching, and database query tuning. If you are a DevOps engineer, backend developer, or CTO evaluating how to future-proof a headless commerce build, these are the areas that will determine how well your store behaves under real traffic load.
If you are still evaluating whether Medusa.js is the right platform for your commerce build, the Askan Technologies eCommerce development overview gives a useful starting point on how headless commerce compares to traditional coupled platforms.
Why Performance Degrades at Scale in Medusa.js
Medusa.js is built on Node.js and PostgreSQL with a modular architecture that gives you a great deal of flexibility. That flexibility is also where performance risks live. As store complexity grows, so does the number of database calls per request, the volume of assets served, and the strain on application servers handling concurrent sessions.
The three most common performance bottlenecks in a scaled Medusa.js deployment are:
Static and semi-static assets being served directly from the application server rather than a CDN edge network
Repeated expensive computations and database reads that could be served from a fast cache layer
Unoptimised SQL queries generating excessive database load, especially on catalogue and order lookup endpoints
Each of these has a well-defined fix. The challenge is understanding which layer is causing the most friction for your specific traffic patterns and addressing them in the right order.
Layer 1: CDN Configuration for a Medusa.js Store
What to Put Behind a CDN
A Content Delivery Network serves assets from edge nodes geographically close to your users, dramatically reducing latency for repeat requests. For a Medusa.js headless storefront, CDN caching is most valuable for:
Product images and media files uploaded through Medusa's file service
JavaScript and CSS bundles from your Next.js or Remix storefront build
Static HTML pages generated at build time via SSG
API responses that return stable, infrequently changing data such as category listings and featured product collections
Recommended CDN Setup
Cloudflare is the most widely used CDN for Medusa.js deployments because of its generous free tier, global edge network, and straightforward cache rule configuration. For production stores with significant traffic, AWS CloudFront paired with S3 for media storage gives you tighter control over cache invalidation and origin configurations.
A basic Cloudflare cache rule for your storefront might look like this:
Cache Rule: /products/* and /collections/*
Cache Level: Cache Everything
Edge Cache TTL: 4 hours
Browser Cache TTL: 30 minutes
Bypass Cache on Cookie: medusa_session (for logged-in users)The session cookie bypass is important. You do not want logged-in users with active carts seeing cached responses that do not reflect their session state.
File Storage Integration
By default, Medusa stores uploaded files locally. For any production deployment, you should replace this with an S3-compatible object storage service so your media files are served through a CDN rather than your application server. Medusa provides a first-party S3 plugin for this purpose.
npm install @medusajs/file-s3Configure the plugin in your medusa-config.js with your S3 bucket details and set your CDN URL as the base URL for media access. This alone can remove a significant proportion of load from your Medusa backend.
Scale your Medusa.js store with us
Let's TalkLayer 2: Redis Caching in Medusa.js
Where Redis Fits in the Architecture
Redis serves two primary roles in a Medusa.js deployment: session storage and application-level caching. Medusa uses Redis for its event bus and workflow engine by default in production, but you can extend Redis caching much further to reduce database round trips on frequently accessed data.
Caching Product and Category Data
Product listings, category trees, and collection data are read far more often than they are written. These are prime candidates for Redis caching with a TTL that aligns with how frequently your catalogue changes.
A practical implementation pattern in a Medusa workflow or custom route:
const cacheKey = `products:category:${categoryId}`
const cached = await redis.get(cacheKey)
if (cached) return JSON.parse(cached)
const products = await productService.list({ category_id: categoryId })
await redis.setex(cacheKey, 3600, JSON.stringify(products))
return productsThis pattern returns cached results instantly for the majority of requests and only hits the database when the cache expires or is explicitly invalidated after a product update.
Cache Invalidation Strategy
Cache invalidation is where most teams make mistakes. A simple and reliable approach for Medusa.js is to hook into Medusa's event system to invalidate relevant cache keys whenever a product or category is updated.
this.eventBusService_.subscribe('product.updated', async ({ id }) => {
const keys = await redis.keys(`products:*${id}*`)
if (keys.length) await redis.del(...keys)
})This ensures your cache never serves stale data after a merchant updates their catalogue, without requiring you to use excessively short TTLs that negate the caching benefit.
Layer 3: Database Query Optimisation
Understanding the Most Expensive Queries
PostgreSQL is a capable database, but unoptimised queries become a serious bottleneck at scale. The first step is visibility: enable query logging in your PostgreSQL configuration to identify slow queries in production.
log_min_duration_statement = 200 # Log queries slower than 200ms
log_statement = 'none'
Once you can see which queries are slow, the fixes usually fall into one of three categories: missing indexes, unnecessary joins pulling in unused columns, and N+1 query patterns.
Adding Indexes for Commerce Queries
Medusa's core tables are well-indexed out of the box, but custom modules and extensions you add may not be. Common index candidates for commerce workloads:
product_variant.product_id for fast variant lookups by parent product
line_item.cart_id and line_item.order_id for cart and order retrieval
customer.email for login and account lookup
order.created_at for date-range reporting queries
Add a partial index on order status if you frequently query open or pending orders separately from completed ones:
CREATE INDEX idx_order_status_pending
ON order (status) WHERE status IN ('pending', 'requires_action');
Avoiding N+1 Queries in Custom Modules
The N+1 problem is particularly common when developers build custom Medusa modules that retrieve a list of records and then loop through them to fetch related data individually. Always use eager loading via TypeORM relations when you know you need associated data.
// Instead of looping and fetching variants one by one:
const products = await productRepo.find({
relations: ['variants', 'variants.prices', 'images'],
where: { collection_id: collectionId }
})
Monitoring and Observability
Optimising without measurement is guesswork. Before and after each change, instrument your Medusa.js application with an observability tool so you can confirm improvements are real and catch regressions early.
For most teams, a combination of these tools works well:
Datadog or New Relic for APM traces on API endpoints, showing response time breakdowns by layer
pganalyze or pgBadger for PostgreSQL query analysis and slow query visualisation
Redis Insights for cache hit rate monitoring and memory usage tracking
Cloudflare Analytics or AWS CloudFront metrics for CDN cache hit ratios and origin request volume
A healthy production Medusa.js store should be targeting cache hit rates above 80% on CDN for static and semi-static content, Redis cache hit rates above 70% for product data, and P95 API response times under 300ms on core product and cart endpoints.
The official Medusa.js documentation at docs.medusajs.com covers the infrastructure and plugin configuration options referenced in this guide and is the most reliable reference for version-specific implementation details.
Putting the Layers Together
CDN, Redis, and database optimisation are not independent switches you flip one at a time. They work as a layered system where each layer handles a different class of request.
A well-optimised Medusa.js store at scale routes requests something like this: the CDN handles the first layer, serving static assets and cached API responses without touching the application at all. Requests that pass through the CDN check Redis next, where frequently accessed dynamic data is returned in sub-millisecond time. Only requests that genuinely require fresh data from the database reach PostgreSQL, and those queries are fast because they are well-indexed and use efficient query patterns.
Getting this architecture right from the start prevents the painful and expensive re-platforming work that growing stores often face when performance problems hit them in production under real traffic.
If you are planning a Medusa.js build or want expert help scaling an existing headless commerce deployment, Askan Technologies works with teams across the stack on Medusa.js architecture, performance, and deployment. You can also explore our DevOps and Cloud services for infrastructure support tailored to headless commerce workloads.
Optimise your headless store performance
Lets TalkWritten by
Kannan Rajendiran
CEO
