Medusa best practices
Medusa is a Node.js headless commerce platform that runs as one or more long-lived Node processes, with Nginx reverse-proxying public traffic to each one. Because there is no dedicated app_type, performance and stability come from how you build, run, and keep those processes running - TurboStack provides the surrounding infrastructure. This page covers what the platform configures for you and how to get the most out of it.
What TurboStack configures for you
When you deploy Medusa with the reverse-proxy pattern, the platform provides the infrastructure around your Node processes:
- Nginx reverse proxy - for every vhost with
proxy_enabled: true, Nginx creates an upstream pointing atproxy_upstream_host(default127.0.0.1) on yourproxy_upstream_port, and proxies all traffic to it. The proxy is tuned with keepalive (up to 2048 pooled connections),proxy_http_version 1.1, and WebSocket pass-through (Upgrade/Connectionheaders), so server-sent events and live admin updates work by default. - Transport Layer Security (TLS) termination -
cert_type: letsencryptissues and auto-renews certificates per domain. Nginx terminates TLS and forwardsX-Forwarded-Proto,X-Forwarded-Host, andSsl-Offloadedheaders so Medusa generates correct absolute URLs. - PostgreSQL - the database Medusa persists all of its data in, provisioned from your
postgresql_version. - Node.js runtime - the
nodejs_versionyou pin per vhost installs the Node runtime each service runs on. - Per-vhost separation - each
app_name(storefront, backend/admin) gets its own document root, Nginx vhost, access log, and upstream. Services run as independent processes on their own ports and domains.
Note
TurboStack configures Nginx reverse proxy, TLS, PostgreSQL, and the Node runtime. The Medusa processes themselves - building, migrating, and keeping them running on the upstream port - are yours to manage.
Recommended optimizations
Combine Medusa's vendor guidance with the TurboStack options below. Most caching and runtime services are one toggle away on the host's Services tab.
- Build before you serve - always run
medusa build(and build the storefront/admin) and start from the compiled output for production. Never run a dev server behind the proxy. - Run in production mode - start each process with
NODE_ENV=productionso Medusa disables dev-only behavior and the admin is served pre-built. - Keep processes running with a process manager - run each Node service under a supervisor (pm2, or a systemd unit). It must restart on crash and on reboot and keep listening on the configured
proxy_upstream_port; if it stops, Nginx returns 502. For example, run the backend and storefront under pm2:pm2 start "npx medusa start" --name medusa-backendandpm2 start "npm start" --name storefront, thenpm2 save. See Keep a Node.js app running for the full pm2 workflow. - Add Redis for events and cache - point Medusa's modules at a Redis instance for the event bus, workflow engine, and cache. This ensures jobs and pub/sub survive restarts and scale beyond a single process. Enable Redis from Services, then set up the cache module - see
Redis cache configuration . - Split server and worker modes - for busy stores, run a dedicated worker process (
workerMode: "worker") alongside the request-serving process (workerMode: "server") so background jobs do not block API responses. - Tune PostgreSQL connection pooling - size the Medusa database pool to your CPU count; avoid exhausting PostgreSQL connections across server and worker processes.
- Serve and cache static assets via a Content Delivery Network (CDN) - place an HTTP cache/CDN in front of the storefront. This prevents the Node process from serving cacheable pages and assets on every request.
- Optimize images and assets - pre-build and compress storefront assets; let Next.js (or your storefront framework) emit optimized, cacheable output.
Redis cache configuration
Medusa's caching module is off by default. Enable it with a feature flag, then point the cache module at the TurboStack cache Redis instance (port 6379, no password). Give each shop its own prefix and a Time to Live (TTL) so cache data stays separate and Redis does not fill up.
Add the variables to the .env file in your project (~/<project>/<shop>/):
# Enable the caching system
MEDUSA_FF_CACHING=true
# Cache Redis instance
CACHE_REDIS_URL=redis://127.0.0.1:6379
# Optional
CACHE_TTL=28800 # 8 hours in seconds
CACHE_PREFIX=example-cache:
Then enable the feature flag and register the Redis cache provider in medusa-config.ts:
import { defineConfig } from "@medusajs/framework/utils"
export default defineConfig({
featureFlags: {
caching: true,
},
modules: [
{
resolve: "@medusajs/medusa/caching",
options: {
providers: [
{
id: "caching-redis",
resolve: "@medusajs/caching-redis",
is_default: true,
options: {
redisUrl: process.env.CACHE_REDIS_URL,
ttl: process.env.CACHE_TTL
? parseInt(process.env.CACHE_TTL, 10)
: undefined,
prefix: process.env.CACHE_PREFIX,
},
},
],
},
},
],
})
Medusa does not cache responses until a query opts in, so enabling the module alone does not populate Redis. A query enables caching through its cache option. Wire up a workflow that runs a cached query, then an API route that calls it.
Create a workflow at ~/<project>/<shop>/src/workflows/cache-products.ts:
import {
createWorkflow,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import { useQueryGraphStep } from "@medusajs/medusa/core-flows"
export const cacheProductsWorkflow = createWorkflow(
"cache-products",
() => {
const { data: products } = useQueryGraphStep({
entity: "product",
fields: ["id", "title"],
options: {
cache: {
enable: true,
providers: ["caching-redis"],
},
},
})
return new WorkflowResponse(products)
}
)
This queries the product entity and caches the response in Redis through the query's cache option.
Then expose it with an API route at ~/<project>/<shop>/src/api/cache-product/route.ts:
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { cacheProductsWorkflow } from "../../workflows/cache-products"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const { result } = await cacheProductsWorkflow(req.scope)
.run({})
res.status(200).json(result)
}
This adds a /cache-product endpoint that runs the workflow and returns the cached product data.
Rebuild and restart after the change. Stop the process first, rebuild, and copy the environment file into the compiled server directory (the build produces a fresh .medusa/server that does not carry your .env):
pm2 stop <process_name>
npx medusa build
cp .env .medusa/server/.env
pm2 restart <process_name>
Click around the storefront or hit the /cache-product endpoint, then confirm keys appear under your CACHE_PREFIX in Redis.
Sizing and scaling
TurboStack auto-tunes resource limits from the host's size, so start with the defaults. Override the tuning variables only when you have measured evidence (slow queries, cache evictions, saturated CPU):
Scale Node throughput by running more processes (server + worker, or multiple server instances on different ports behind Nginx) before scaling the host. See Performance tuning before changing any tuning variable, and scale up the host when a single machine can no longer keep up.
Stability
- Back up regularly - verify scheduled Backups cover the PostgreSQL database and any uploaded media/file storage.
- Watch Health - monitor CPU, memory, and disk. A crash-looping Node process or a stuck migration appears here first.
- Run migrations on every deploy - apply
medusa db:migratebefore starting new code so the schema matches the running version. - Keep versions current - track Medusa releases and keep your pinned
nodejs_versionandpostgresql_versionon supported lines. - Test on a staging clone - trial upgrades, plugins, and migrations on a copy before publishing to production.