# 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 at `proxy_upstream_host` (default `127.0.0.1`) on your `proxy_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`/`Connection` headers), so server-sent events and live admin updates work by default.
- **Transport Layer Security (TLS) termination** - `cert_type: letsencrypt` issues and auto-renews certificates per domain. Nginx terminates TLS and forwards `X-Forwarded-Proto`, `X-Forwarded-Host`, and `Ssl-Offloaded` headers 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_version` you 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](../../platform/hosts/services.md) 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=production` so 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-backend` and `pm2 start "npm start" --name storefront`, then `pm2 save`. See [Keep a Node.js app running](../../technologies/nodejs/run-with-process-manager.md) 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](../../platform/hosts/services.md), then set up the cache module - see [Redis cache configuration](#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>/`):

```bash
# 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`:

```javascript
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`:

```javascript
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`:

```javascript
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`):

```bash
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):

| Variable | Tune when |
| --- | --- |
| `postgresql` resources | The database is the bottleneck under order/catalog load |
| `redis_memory` | Redis is evicting keys used for events or cache |

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](../../concepts/performance-tuning.md) 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](../../platform/hosts/backups.md) cover the PostgreSQL database and any uploaded media/file storage.
- **Watch [Health](../../platform/hosts/health.md)** - 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:migrate` before starting new code so the schema matches the running version.
- **Keep versions current** - track Medusa releases and keep your pinned `nodejs_version` and `postgresql_version` on supported lines.
- **Test on a staging clone** - trial upgrades, plugins, and migrations on a copy before publishing to production.

## Related

- [Deploy Medusa](deploy.md)
- [Troubleshooting Medusa](troubleshooting.md)
- [How to manage user system services](../../technologies/system-services/manage-user-services.md)
- [Services](../../platform/hosts/services.md)
- [Performance tuning](../../concepts/performance-tuning.md)
