A Medusa.js store that runs fast in development and crawls in production is not a Medusa.js problem. It is an infrastructure and optimization problem that many teams simply do not plan for until users start complaining. API response times directly shape conversion rates. A one-second delay in page load can cost a meaningful percentage of sales, and a sluggish checkout flow is one of the most reliable ways to lose a customer who was already ready to buy.

    This guide covers the three performance levers that matter most for a production Medusa.js deployment: Redis-based caching, CDN configuration for static and semi-static content, and PostgreSQL query tuning. Each section includes concrete implementation steps you can apply to a live store without a full architecture rewrite. The goal is not theoretical performance; it is measurable improvement in p99 latency, database load, and storefront time-to-first-byte.

    Why Performance Is a Shared Responsibility in Headless Commerce

    Headless platforms like Medusa.js decouple the frontend from the backend, which gives you enormous flexibility but also means performance is entirely in your hands. Managed SaaS platforms like Shopify handle server provisioning and CDN routing for you, at the cost of customization. With Medusa.js, you own the infrastructure decisions, and those decisions have a direct and measurable impact on your store's speed.

    The good news is that a well-optimized Medusa.js deployment can be significantly faster than a comparable Shopify store because you are not sharing infrastructure with hundreds of thousands of other merchants. The bad news is that achieving that performance requires deliberate work across three distinct layers: the application cache, the content delivery network, and the database. Skipping any one of them leaves performance on the table.

    Layer 1: Redis Caching in Medusa.js v2

    The Native Caching Module

    Medusa.js introduced a dedicated Caching Module starting with v2.11.0, replacing the older Cache Module that is now deprecated. The new module is provider-based, meaning you register one or more caching backends and Medusa routes cache operations through them. Redis is the recommended provider for any production deployment because it is centralized, persistent across restarts, and shared across all application instances when you run multiple server processes behind a load balancer.

    To enable Redis caching, you first set the feature flag in your environment file and then register the Redis Caching Module Provider in your medusa-config.ts. The official documentation at docs.medusajs.com/resources/infrastructure-modules/caching covers the full configuration options. The essential setup looks like this:

    javascript
    MEDUSA_FF_CACHING=true
    CACHE_REDIS_URL=redis://your-redis-host:6379

    In your medusa-config.ts, register the module under the modules array:

    javascript
    resolve: "@medusajs/medusa/caching",
    providers: [{ resolve: "@medusajs/caching-redis", id: "caching-redis", is_default: true,
      options: { redisUrl: process.env.CACHE_REDIS_URL } }]

    What to Cache and What to Skip

    Not all data should be cached equally. The decision comes down to how frequently the data changes and what the cost of serving stale data is. Product listings, category pages, and pricing data for non-logged-in users are excellent caching candidates because they change infrequently relative to how often they are read. Cart data, real-time inventory counts, and any user-specific data should never be cached globally.

    Cache Invalidation Strategy

    Cache invalidation is where most caching implementations go wrong. Medusa.js supports event-driven cache invalidation through its Subscriber system. When a product is updated, you subscribe to the product.updated event and clear the corresponding cache key. This ensures that cached data is only stale within your TTL window, not indefinitely after a write operation.

    Use namespaced cache keys to make bulk invalidation straightforward. A key pattern like medusa:cache:product:list or medusa:cache:category:electronics groups related cache entries under a predictable prefix, so you can clear an entire namespace when a batch update occurs without iterating over every individual key.

    Need Help Configuring Redis Caching for Your Medusa.js Store?

    Talk to Our Performance Specialists at Askan Ecomm
    Let's Talk

    Layer 2: CDN Configuration for Global Performance

    What the CDN Handles

    A CDN solves a different problem than Redis. Redis reduces the number of database reads your application server has to perform. A CDN reduces how far a network request has to travel before it reaches the nearest server with the data. For a global storefront, the physical distance between a user in Frankfurt and a server in Mumbai is measured in hundreds of milliseconds. A CDN with edge nodes in Europe eliminates that round-trip entirely for cacheable content.

    For Medusa.js storefronts built on Next.js, Cloudflare and AWS CloudFront are the two most common CDN choices. Both support edge caching of API responses, static assets, and server-rendered pages. Cloudflare is particularly popular for its ease of configuration and the free tier that covers many small-to-medium stores. CloudFront integrates more tightly with AWS infrastructure if you are already hosting Medusa.js on EC2 or ECS.

    Static Assets and Image Optimization

    Static assets including JavaScript bundles, CSS files, fonts, and product images should be served from the CDN with long-lived cache headers. A Cache-Control header of max-age=31536000, immutable is appropriate for hashed asset filenames because the hash changes whenever the file changes, making stale-cache errors impossible. Product images should be served through an image optimization pipeline that generates appropriately sized WebP variants, reducing payload size by 30 to 70 percent compared to unoptimized JPEG or PNG delivery.

    Next.js's built-in Image component handles automatic WebP conversion and size-based srcset generation when configured with a CDN origin. Configure the images.domains or images.remotePatterns setting in your Next.js config to point to your Medusa file storage bucket URL, and the Image component will proxy and optimize images through the Next.js image optimization API before they are cached at the CDN edge.

    API Response Caching at the Edge

    For public API endpoints that return the same data for all unauthenticated users, such as product listings and category data, you can cache API responses directly at the CDN edge rather than relying solely on Redis. The CDN acts as the outermost cache layer, meaning requests that hit the CDN edge never reach your Medusa.js server at all. This dramatically reduces origin traffic during traffic spikes and significantly improves p99 latency for global users.

    Configure cache rules to set short TTLs on dynamic product endpoints (30 to 60 seconds) and longer TTLs on more stable content like navigation menus or region configuration (5 to 15 minutes). Always include a Surrogate-Key or Cache-Tag header on API responses so you can perform targeted CDN cache purges when specific content updates rather than clearing the entire cache.

    Layer 3: PostgreSQL Query Tuning

    Start with Measurement, Not Guesses

    The most important rule of database performance tuning is to measure before you optimize. Adding indexes blindly or rewriting queries without data is slow and unreliable. PostgreSQL's pg_stat_statements extension tracks the execution statistics of every query that runs against your database, including total execution time, average execution time, and call count. Enabling this extension and sorting queries by total execution time immediately shows you where the actual bottlenecks are.

    Once you have identified a slow query, use PostgreSQL's EXPLAIN (ANALYZE, BUFFERS) command to see exactly what the query planner is doing. Look for sequential scans on large tables, which indicate that the query is reading every row rather than using an index. Sequential scans on tables with more than a few thousand rows are almost always a sign that an index is missing or that the query is structured in a way that prevents index use.

    Indexing Common Medusa.js Query Patterns

    Medusa.js creates standard indexes on primary keys and foreign keys through its migrations, but as your store grows and you add custom data models, you will need to add indexes manually for columns that appear frequently in WHERE clauses, ORDER BY expressions, and JOIN conditions. The most impactful indexes for a typical Medusa.js store target the following columns:

    • orders.status and orders.created_at: Most order list queries filter by status and sort by creation date. A composite index on both columns covers these queries efficiently.

    • product.handle: Product detail pages look up products by handle. This column should have a unique index, which Medusa.js creates by default, but verify it is present after any custom migrations.

    • line_item.cart_id: Cart retrieval queries join line items to their parent cart. This foreign key column should be indexed for stores with high cart abandonment rates and large line item tables.

    • customer.email: Customer lookup by email happens during login and order history retrieval. A unique index on this column prevents full table scans on large customer datasets.

    • Custom module columns: Any column in a custom data model that appears in a filter or sort parameter in your API routes should be indexed through a Medusa migration.

    Eliminating N+1 Query Problems

    The N+1 query problem occurs when code retrieves a list of N items and then executes an additional database query for each item to fetch related data, resulting in N+1 total queries instead of the 2 that a properly joined query would require. In Medusa.js, this most commonly appears when fetching product lists with variant data, where a naive implementation loads products and then fetches variants one product at a time.

    Medusa.js's Query API, documented at docs.medusajs.com/learn/fundamentals/module-links/query, handles relation loading efficiently when you specify the fields and relations you need upfront. Always define the relations array in your Query call to fetch related data in a single joined query rather than loading it lazily through multiple round-trips.

    When writing custom queries using MikroORM's entity manager directly, use leftJoinAndSelect or populate options to preload relations in a single database statement. Audit your custom module services for any loop that calls a service method or repository method inside a forEach or map function, as these patterns are a common source of N+1 issues in custom Medusa.js code.

    Connection Pooling with PgBouncer

    Opening a new PostgreSQL connection is an expensive operation that can consume 2 to 10MB of RAM per process. In a serverless or container-based deployment where many application instances spin up and down dynamically, connection storms can overwhelm a PostgreSQL instance even when the actual query volume is modest. PgBouncer acts as a connection pool proxy that sits between your Medusa.js application and PostgreSQL, maintaining a smaller pool of persistent connections and queuing application requests against that pool.

    Configure PgBouncer in transaction mode for Medusa.js workloads. Transaction mode recycles connections after each transaction completes rather than holding them for the duration of a session, which significantly reduces the number of actual PostgreSQL backend processes required to serve high concurrency. A PgBouncer pool of 20 to 50 connections can support hundreds of concurrent Medusa.js application requests that would otherwise each require their own PostgreSQL process.

    What to Monitor

    Recommended Tool

    Alert Threshold

    API p99 latency

    Grafana or Datadog

    Above 500ms for product endpoints

    PostgreSQL slow queries

    pg_stat_statements + pganalyze

    Any query averaging above 100ms

    CDN cache hit ratio

    Cloudflare Analytics or CloudFront metrics

    Below 85% for public endpoints

    Putting It All Together: A Practical Optimization Sequence

    If you are starting from a store that has no deliberate performance optimization in place, the most efficient sequence is to address the layers in order of implementation effort versus impact. Redis caching delivers the highest return for the least infrastructure change. CDN configuration requires more setup but has an outsized impact on global users and high-traffic periods. PostgreSQL tuning is the most technically involved but often uncovers problems that no amount of caching can fully compensate for.

    • Week 1: Enable Redis Caching Module, configure TTLs for product and category endpoints, verify cache hit rate is above 80 percent.

    • Week 2: Enable pg_stat_statements, identify the five slowest queries, add missing indexes, test with EXPLAIN ANALYZE.

    • Week 3: Configure CDN with appropriate cache rules for public API endpoints and static assets, verify global TTFB improvement.

    • Week 4: Set up monitoring dashboards, configure alerts, establish a baseline for ongoing performance tracking.

    The Askan Ecomm team has worked through this optimization sequence on multiple production Medusa.js deployments. You can read about the broader architectural decisions that underpin a well-performing Medusa.js store in the earlier article on Medusa.js Architecture Explained, and for a comparison of how Medusa.js performance characteristics stack up against a managed SaaS platform, the Medusa.js vs Shopify comparison guide covers that trade-off in detail.

    For teams that want to go deeper on the infrastructure side, the Askan Technologies engineering blog covers API architecture patterns and backend scalability approaches at askantech.com/medusa-js-ecommerce-development-company, which is relevant context when you are making decisions about deployment topology for a high-traffic Medusa.js store.

    R

    Written by

    Raj Thilak

    CQO

    Ready to Transform
    Your Business?

    Build your next landing page fast & easy

    Available now

    Free consultation included. We'll review your requirements and provide a detailed proposal.