Your Medusa v2 store works fine in development — 50 products, 200 variants, checkout takes 300ms. You launch. A month later, the catalogue grows to 500 products with 5000 variants. Checkout now takes 8 seconds. Customers abandon. The product listing page takes 4 seconds. The root cause is not your server hardware — it is a quadratic variant pricing lookup that scans the entire pricing table for every variant in the response. GitHub #10751 reports the exact same degradation. This guide diagnoses the bottleneck with an interactive profiler, shows the database indexes that fix it, and adds a caching layer and pagination guard to keep response times under 100ms regardless of catalogue size.
Running Medusa on affordable infrastructure?
Performance fixes only help if your server is not the bottleneck. The Coolify + Hetzner deployment guide covers the production-hardened setup, and the Razorpay and COD + Shiprocket guides wire the India payment and fulfillment stack.
The performance cliff is not linear — it is quadratic. Doubling your variant count does not double your response time; it quadruples it. A store with 1000 variants might be tolerable at 800ms. At 5000 variants, the same query takes 8+ seconds because the pricing module joins every variant against every price set without a covering index. I am Manoj, commerce and ERP implementation lead at Mith Tech in Bengaluru, and diagnosing performance bottlenecks in headless commerce stacks is what keeps our clients' conversion rates intact.
Why checkout gets slow — the quadratic scan
When Medusa v2 resolves prices for product variants, it joins the product_variant table against product_variant_price_set, then against price_set_money_amount, and optionally against price_rule for region-specific pricing. Without indexes on these joins, PostgreSQL falls back to sequential scans.
For a single variant, this is invisible — scanning a few hundred rows is fast. But when the storefront requests a product listing with 100 variants, each variant triggers its own join cascade. The total work is proportional to variants × price_sets × money_amounts — effectively O(n²). With 1000 variants and 3 price sets per variant, that is 3000 sequential scans.
The Variant Scale Simulator shows this degradation:
Select a variant count to see how response time scales with and without database indexes. The quadratic curve matches the real-world degradation reported in GitHub #10751.
Variant Scale Simulator
See how response time degrades with naive O(n²) variant lookup vs indexed O(1). Based on reports from GitHub #10751 and #16232.
Naive O(n²)
100 ms
Quadratic scan
Indexed O(1)
2 ms
Hash lookup
Speedup
50x
Faster with index
Diagnosing your store
Not every Medusa store hits this bottleneck. Stores with under 500 variants and simple pricing (one price per variant, no price lists, no region overrides) may never notice. The Checkout Profiler walks through the five risk factors:
Answer five questions about your store configuration to see whether the variant pricing bottleneck affects you — and which fixes to prioritise.
Checkout Performance Profiler
Click each check to cycle through: unchecked → yes (applies) → no (does not apply).
The fix — indexes, cache, pagination
The fix is three layers, each addressing a different access pattern:
- Database indexes fix the root cause — the sequential scans in the pricing joins. This is the highest-impact change and should be applied first.
- Variant cache eliminates redundant lookups within a single request. When a cart calculation resolves the same variant's price multiple times (e.g., for tax calculation, discount calculation, and total calculation), the cache serves it from memory.
- Pagination guard prevents the worst case — a storefront request that fetches all variants without a limit (GitHub #12481).
Three-tab code recipe: the SQL migration for database indexes, the in-memory LRU cache, and the pagination guard middleware. Apply all three for stores with 500+ variants.
Performance Fix Recipes
-- Migration: add indexes for variant pricing lookup
-- Addresses the O(n²) scan in GET /store/products/:id/variants
-- See: GitHub #10751, #16232
CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_product_variant_price_set
ON product_variant_price_set (variant_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_price_set_money_amount
ON price_set_money_amount (price_set_id, currency_code);
CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_money_amount_price_list
ON money_amount (price_list_id)
WHERE price_list_id IS NOT NULL;
-- For region-specific pricing
CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_price_rule_price_set
ON price_rule (price_set_id, rule_attribute);Run the index migration
Create a new migration file with the four CREATE INDEX CONCURRENTLY statements. The CONCURRENTLY keyword is critical — it builds the index without locking the table, so your production store stays online. Run medusa db:migrate or apply directly via psql. On a table with 50,000 price rows, expect 10–30 seconds for all four indexes.
Add the variant cache
Create src/utils/variant-cache.ts with the LRU cache implementation. Import it in your pricing-related services. The 30-second TTL means prices update within 30 seconds of changes in the admin — acceptable for most stores. Increase MAX_ENTRIES if your catalogue has more than 2000 active variants.
Add the pagination middleware
Create or update src/api/middlewares.ts with the pagination guard. The 100-variant cap is a safe default — most product pages show 20–50 variants at a time. Your storefront should paginate if it needs more. This also reduces response payload size, which improves time-to-first-paint.
Verify the improvement
Run the same queries before and after. Use EXPLAIN ANALYZE on the variant pricing query to confirm indexes are being used. Check the Medusa response times via browser DevTools or a monitoring tool. The Before/After Benchmark below shows typical improvements.
Before vs after
The benchmark below shows typical response times for a Medusa v2 store with 1000 product variants, measured on a Hetzner CAX21 (4 ARM cores, 8GB RAM):
Response time comparison for key operations — variant listing, cart operations, and checkout — before and after applying database indexes, caching, and pagination.
Performance Benchmark
Response times for a store with 1,000+ product variants — before and after applying database indexes, variant caching, and pagination guards.
| Operation | Before | Status |
|---|---|---|
| List 1,000 variants | 3,200 ms | Degraded |
| Add to cart (variant lookup) | 850 ms | Degraded |
| Calculate cart totals | 1,200 ms | Degraded |
| Checkout complete | 4,500 ms | Degraded |
Avg before
2,438 ms
No indexes, no cache
Avg after
53 ms
Indexed + cached + paginated
What to know before you commit
Index maintenance. Indexes speed up reads but slow down writes. Each INSERT or UPDATE to the price tables must also update four indexes. For most Medusa stores, price updates are infrequent (admin operations) while reads are constant (every storefront request), so the trade-off is heavily in favour of indexes. If you bulk-import prices frequently, run REINDEX CONCURRENTLY monthly.
Cache invalidation. The 30-second TTL means a price change in the admin takes up to 30 seconds to reflect on the storefront. For stores where real-time pricing matters (flash sales, dynamic pricing), reduce the TTL to 5 seconds or implement event-driven invalidation via Medusa's subscriber system.
PostgreSQL version. CREATE INDEX CONCURRENTLY requires PostgreSQL 11+. If you are on an older version (unlikely with Medusa v2, which requires PostgreSQL 14+), remove the CONCURRENTLY keyword — but expect table locks during index creation.
The medusa db:migrate ARM64 hang (GitHub #16011) can affect the migration on ARM-based servers. If the migration hangs, apply the indexes directly via psql instead of through the Medusa migration system.
+Why is my Medusa v2 checkout slow with many variants?
The pricing module performs a quadratic scan — each variant lookup joins against the full price_set_money_amount table without covering indexes. With 1000+ variants, this results in millions of row comparisons per request. Adding database indexes on the join columns reduces this to indexed lookups, cutting response times by 70–90%.
+How many variants can Medusa v2 handle?
With default configuration, Medusa v2 starts degrading at 500+ variants (response times above 1 second) and becomes unusable at 5000+ variants (8+ second responses). With proper database indexes, caching, and pagination, stores handle 10,000+ variants with sub-100ms response times. The bottleneck is the pricing lookup, not Medusa's architecture.
+Do I need to change Medusa source code to fix checkout performance?
No. The fix is entirely external to Medusa's core: a SQL migration for database indexes (applied once), an optional caching utility, and an API middleware for pagination. You do not need to fork or patch Medusa. The indexes work with any v2.x release because the pricing table schema has been stable since v2.0.
+Will database indexes slow down price updates?
Marginally. Each price insert or update must also update four indexes, adding a few milliseconds per write operation. For most stores, price updates happen infrequently (admin operations) while reads happen constantly (every storefront request), so the read improvement far outweighs the write overhead.
+How do I check if my Medusa store needs these indexes?
Run EXPLAIN ANALYZE on a variant pricing query and look for "Seq Scan" in the output. If you see sequential scans on product_variant_price_set or price_set_money_amount, the indexes are missing. Alternatively, use the Checkout Profiler above to assess your risk factors based on variant count, pricing strategy, and cart size.
+What is the medusa db:migrate ARM64 hang?
On ARM64 Linux servers (Hetzner CAX, AWS Graviton, Oracle Ampere), medusa db:migrate can hang indefinitely during migration execution (GitHub #16011). The workaround is to apply migrations directly via psql or use the --no-interaction flag. This affects the migration system, not the indexes themselves.
+Should I use Redis instead of in-memory caching?
For a single Medusa instance, in-memory caching is simpler and faster (no network hop). For multiple Medusa instances behind a load balancer, use Redis so all instances share the same cache. Medusa v2 supports Redis as a cache backend via the @medusajs/cache-redis module — configure it in medusa-config.ts.
+What Medusa version does this guide apply to?
This guide targets Medusa v2.18.0 (latest stable as of August 2026). The pricing table schema (product_variant_price_set, price_set_money_amount, money_amount, price_rule) has been stable since v2.0. The indexes and middleware work across v2.x releases. Check Medusa release notes for any core pricing performance improvements that may overlap with these fixes.
Checkout performance is a conversion problem, not a DevOps problem. The quadratic variant pricing lookup is the single biggest performance bottleneck in Medusa v2 stores that grow past 500 variants. Four SQL indexes fix the root cause. The cache and pagination guard handle the edge cases. If you run EXPLAIN ANALYZE before and after, you will see sequential scans disappear and response times drop from seconds to milliseconds.
Medusa checkout too slow for your catalogue?
We profile and fix performance bottlenecks in headless commerce stacks. If your Medusa store is slowing down as the catalogue grows, we will find the root cause and fix it — not guess at it.