Skip to main content
Production Next.js · Blog 25

Next.js 16 Production Scaling

Move from one Next.js process to a resilient multi-instance architecture without losing sessions, exhausting PostgreSQL, splitting caches, or duplicating background work.

Production architecture with a reverse proxy, three application containers, Redis, PostgreSQL, workers and monitoring

In Blog #24, we added monitoring and observability so we can see how production behaves. Those signals tell us whether scaling is necessary. This guide uses them to move safely from one Next.js instance to multiple application and worker instances.

Repository architecture audit

This publishing repository is a PHP/Apache website, not the example Next.js runtime. It contains no root Next.js package, Dockerfile, Compose stack, Traefik service, PostgreSQL connection, Redis client, session store, queue worker, health endpoint, migration runner, upload service, or deployment workflow to scale. A downloadable starter declares a floating next: latest, so an exact patch version cannot be verified. The examples below are educational blueprints; no imaginary production containers were launched.

How Do You Scale a Next.js Application?

A Next.js application scales horizontally by running identical instances behind a reverse proxy or load balancer. Safe production scaling also requires stateless requests, shared session and cache coordination, bounded database connections, durable file storage, health checks, independently scaled workers, observable capacity, and deployment practices that tolerate overlapping releases.

Scaling Architecture at a Glance

Diagram 1: Multi-instance production architectureUsers enter through one edge, healthy application replicas share PostgreSQL and Redis, and queue consumers scale independently.

What Does Scaling Mean?

Scaling increases useful capacity or fault tolerance. Vertical scaling gives one instance more CPU or memory. Horizontal scaling adds instances and distributes work. Vertical changes are simpler, but one large process remains one failure domain. Horizontal scaling improves redundancy and concurrency while introducing coordination costs.

Scale up

More CPU, RAM, faster disks, or larger managed services. Start here when the workload is small and operational simplicity matters.

Scale out

More web or worker replicas. Use it when measured concurrency, availability goals, or independent workloads justify the coordination overhead.

When Should You Scale?

Scale from evidence, not traffic anxiety. Look for sustained CPU saturation, memory or event-loop pressure, rising p95 latency, request queues, errors caused by capacity, database pool waits, Redis latency, or job age that breaches a delivery promise. A resilience requirement may justify two instances before raw load does.

Optimize Before Scaling

Fix slow queries and missing indexes, remove request waterfalls, stream useful UI, reduce client JavaScript, cache only where correctness permits, compress assets, and move long work to queues. Blog #14 explains this measurement-first loop. Multiplying inefficient code multiplies cost and dependency pressure.

Single-Instance vs Multi-Instance Architecture

ConcernSingle instanceMultiple instances
TrafficOne processLoad-balanced replicas
MemoryProcess-localNever assume shared memory
CacheLocal cache can be sufficientShared handler and tag coordination may be required
FilesLocal disk may appear durableUse object/shared storage
DatabaseOne connection poolPool budget multiplied by replicas
JobsOne consumerIdempotent competing consumers

Stateless Next.js Applications

Any healthy instance should be able to serve the next request. Keep durable business state in PostgreSQL, shared short-lived state in Redis when appropriate, and files in durable object storage. Do not store sessions, rate counters, locks, uploads, or job ownership only in process memory or a container filesystem.

Multi-server invariants

Current Next.js self-hosting guidance calls for the same build across instances, a consistent NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, a stable deploymentId for version-skew protection, and coordinated cache/tag behavior when the application depends on shared revalidation.

Load Balancing with Traefik

A reverse proxy terminates TLS, applies reviewed forwarding policy, and sends requests only to ready backends. With the Docker provider, Traefik can discover replicas by labels and network membership. Set an explicit application port and network, disable exposure by default, protect the Docker API, and preserve streaming rather than buffering every response.

Illustrative Compose service—audit before use
services:
  app:
    image: registry.example/app:${APP_VERSION:?set APP_VERSION}
    restart: unless-stopped
    expose: ["3000"]
    networks: [edge, data]
    healthcheck:
      test: ["CMD", "node", "healthcheck.mjs"]
      interval: 15s
      timeout: 3s
      retries: 3
    labels:
      - traefik.enable=true
      - traefik.docker.network=edge
      - traefik.http.services.app.loadbalancer.server.port=3000
      - traefik.http.services.app.loadbalancer.healthcheck.path=/api/health/ready
      - traefik.http.services.app.loadbalancer.healthcheck.interval=10s

Run a validated service with docker compose up -d --scale app=3. Do not set container_name or publish the same host port on every replica. Compose service discovery uses stable service names while container IP addresses can change.

Load balancer routing requests to two healthy containers while removing one failed container from rotation
Health-aware routing. Readiness removes an unhealthy instance while traffic continues to replicas that can serve it.

Health Checks and Graceful Shutdown

Liveness answers whether the process should restart; readiness answers whether it should receive traffic. Liveness should not fail because PostgreSQL briefly slowed. Readiness may use tightly timed critical checks, return 503 while draining, and reveal no hostnames, versions, secrets, or dependency errors.

Startup

Start the process, warm only essential state, then become ready. Never accept traffic before required initialization completes.

Shutdown

Mark unready, stop accepting new work, finish bounded in-flight requests or jobs, close pools, then exit before the platform timeout.

Sessions, Authentication, and Rate Limiting

Signed, encrypted, bounded cookies can be validated by every replica when keys are consistent. Stateful sessions belong in a shared store. Redis-backed rate counters must use atomic operations, expiry, normalized identities, and a defined fail-open or fail-closed policy. Proxy-derived client IP is trustworthy only when the proxy chain and forwarded-header policy are controlled. Sticky sessions can bridge a legacy design, but they do not coordinate caches, files, jobs, or deployments.

Shared State and Next.js Caching

Blog #22 introduced Redis for shared cache and rate limits. In a multi-instance Next.js deployment, the default in-memory cache is isolated per process. Current guidance supports custom cacheHandler behavior for server response/ISR cache and cacheHandlers for Cache Components. If tag invalidation must reach every instance, coordinate it through shared storage and refreshTags().

Do not turn Redis into an accidental source of truth. Separate cache, session, rate-limit, and queue keyspaces; use TTLs; bound key cardinality; choose maxmemory and eviction for each workload; and select persistence or replication from recovery objectives. Queue or session data should not disappear because cache entries were allowed to evict.

PostgreSQL Connection Scaling

Database capacity rarely grows at the same rate as containers. If each of 6 application replicas opens a 10-connection pool and 4 workers open 5 more, the planned maximum is already 80 connections before migrations, administration, monitoring, failover, or surge headroom.

Connection budget(app replicas × app pool) + (workers × worker pool) + operations + headroom ≤ database limit

Use a provider-supported pooler when appropriate, cap every process pool, set connection and statement timeouts, release clients in finally, and monitor checked-out, idle, waiting, and rejected connections. Improve indexes and query plans before adding read replicas. Read replicas introduce lag and read-after-write consistency decisions; route only explicitly safe reads.

Scaling Background Workers and Queue Backpressure

Blog #23 separated request producers from workers. Scale consumers using arrival rate, oldest job age, runtime percentiles, retries, and downstream capacity. Jobs must be idempotent, acknowledge only after durable completion, retry with bounded exponential backoff and jitter, and move exhausted work to a reviewed failure path.

Three stateless web containers sharing Redis, a PostgreSQL connection pool and a durable queue with two workers
Shared boundaries, independent capacity. Web replicas, the database pool, shared state, and workers are scaled and monitored as different resources.
Diagram 2: Backpressure before overloadAdmission control and a durable queue protect a slower downstream dependency from unlimited concurrency.

File Uploads, Image Processing, and Shared Storage

Container-local files are neither shared nor durable. Stream validated uploads to object storage, record durable metadata in PostgreSQL, use signed URLs with narrow scope, and process expensive transformations asynchronously. Enforce type, size, dimension, authorization, malware policy, and lifecycle limits before work fans out. A CDN can serve immutable public assets, but private responses require correct cache keys and authorization boundaries.

Scheduled Jobs and Database Migrations

Putting a cron loop inside every app replica duplicates work. Use one external scheduler that enqueues uniquely identified jobs, or a distributed lease with expiry and fencing. Run forward-compatible migrations as a controlled release step, not from every container startup. Prefer expand-and-contract changes so old and new releases can overlap safely.

Zero-Downtime and Rolling Deployments

  1. Build one immutable image and promote the same artifact.
  2. Apply reviewed backward-compatible migrations once.
  3. Start new replicas with the same Server Actions key and a new stable deployment ID.
  4. Wait for readiness and run smoke checks.
  5. Shift traffic gradually while watching errors, latency, pool waits, and queue age.
  6. Drain old web and worker processes before termination.
  7. Rollback the application independently of destructive schema changes.

Version skew can break assets, prefetched data, or Server Functions when clients cross releases. Next.js deploymentId helps the client detect mismatches and perform a hard navigation. Keep previous immutable assets available for the overlap window when the delivery architecture requires it.

Monitoring Scaling Decisions and Capacity Planning

Continue the Blog #24 observability model: request rate, error ratio, p50/p95/p99 latency, CPU, memory, event-loop delay, restarts, readiness transitions, active database connections, pool wait, Redis memory and evictions, queue age, throughput, retry rate, and deployment markers. Load test representative routes and jobs with safe synthetic data, then document the saturation point and operating headroom.

Web bottleneck

Add app capacity only if the database, Redis, proxy, and downstream budgets can absorb it.

Queue bottleneck

Add workers only while oldest-job age falls and downstream error/latency stays healthy.

Database bottleneck

Tune queries, indexes, transactions, and pooling before adding more callers.

Cache bottleneck

Inspect hit ratio, hot keys, latency, memory, eviction, and correctness before sharding.

Failure Scenarios and High Availability

FailureExpected behaviorTest
One app exitsReadiness removes it; other replicas serve trafficTerminate one replica under load
Redis pausesDefined cache/session/queue behavior; bounded retriesInject timeout and reconnect
PostgreSQL failoverPools reconnect; requests remain boundedProvider-approved failover drill
Worker crashesUnacknowledged job retries without duplicate effectExit mid-job
Release is badHealth gates stop rollout; immutable rollback worksStaged canary failure
Host is lostCapacity exists in another failure domainControlled host/zone exercise

Three containers on one server improve process availability, not host availability. True high availability separates failure domains and includes replicated data services, restore-tested backups, DNS/TLS recovery, capacity during failure, and rehearsed runbooks.

Scaling Security

  • Expose only the reverse proxy publicly.
  • Keep data networks private and use service names, not fixed IPs.
  • Use least-privilege database and Redis credentials.
  • Rotate secrets without logging them or baking them into images.
  • Authenticate internal administration and metrics endpoints.
  • Limit request bodies, concurrency, timeouts, and outbound access.
  • Patch immutable base images and scan the delivered artifact.
  • Protect Docker socket access and restrict Traefik discovery.
  • Redact cookies, tokens, connection URLs, and user payloads from telemetry.

Common Scaling Mistakes

Scaling without measurements

Replicas hide the first bottleneck and create a larger bill.

Local sessions or files

The next request reaches another instance or the container is replaced.

Pool multiplication

A safe per-process pool becomes an unsafe global connection count.

Independent cache invalidation

One instance revalidates while another continues serving stale output.

Every replica runs cron

Scheduled work executes multiple times without idempotency.

Workers scaled without limits

Downstream services throttle, lock, or fail under extra concurrency.

Startup migrations

Several instances race through schema changes during a rollout.

One-host “HA”

All replicas disappear with the same machine or network.

Next.js Production Scaling Best Practices

  • Optimize and measure before adding replicas.
  • Use one immutable build across the release.
  • Keep request handling stateless.
  • Coordinate Server Actions keys, deployment identity, cache storage, and tag invalidation.
  • Prefer shared sessions over sticky routing.
  • Plan PostgreSQL connections across every process.
  • Separate Redis workloads and memory policies.
  • Store files outside disposable containers.
  • Scale web and worker capacity independently.
  • Make jobs and scheduled work idempotent.
  • Separate liveness, readiness, and startup behavior.
  • Drain processes during rolling deployment.
  • Test dependency, replica, host, and rollback failures.
  • Keep enough headroom to survive the planned failure domain.

Frequently Asked Questions

How do you scale a Next.js application?

Run identical application instances behind a reverse proxy or load balancer, keep request handling stateless, coordinate caches and shared state, cap database connections, scale workers separately, and use readiness checks plus monitoring to control deployments.

Can Next.js run in multiple containers?

Yes. Use the same build, Server Actions encryption key, deployment identifier, runtime configuration, and coordinated cache strategy across instances.

Does a load-balanced Next.js application need sticky sessions?

Usually no. Prefer stateless authentication or a shared session store. Sticky sessions can be a temporary compatibility measure but reduce flexibility and do not solve cache or deployment coordination.

Why does PostgreSQL pooling matter when scaling Next.js?

Every process can create its own pool. Multiplying the per-process pool by all app and worker replicas can exceed the database connection budget, so capacity must be planned globally.

Should Redis store every kind of state?

No. Redis is useful for bounded shared cache, rate limits, sessions, coordination, and queues. Durable business records normally belong in PostgreSQL, with Redis persistence and eviction chosen per workload.

How should background workers scale?

Scale worker concurrency from queue age, arrival rate, processing time, downstream limits, and idempotency. More workers can make a constrained database or provider slower.

Where should uploaded files go in a multi-instance deployment?

Use reviewed object or shared storage and persist only durable metadata in the database. Container-local files are not reliably visible to other instances and disappear when containers are replaced.

When should a Next.js application scale horizontally?

Scale after measurements show sustained saturation or resilience requirements that optimization and vertical capacity cannot address. CPU, memory, latency, error rate, queue age, and database pool pressure should guide the decision.

Current Official References

Next Steps

Start with the measured bottleneck and write down the shared-state, connection, file, cache, job, health, migration, and shutdown behavior before adding a second instance. Then test one controlled failure at a time. Scaling is successful when capacity increases without weakening correctness, security, or recovery.

WhatsApp