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.
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
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
| Concern | Single instance | Multiple instances |
|---|---|---|
| Traffic | One process | Load-balanced replicas |
| Memory | Process-local | Never assume shared memory |
| Cache | Local cache can be sufficient | Shared handler and tag coordination may be required |
| Files | Local disk may appear durable | Use object/shared storage |
| Database | One connection pool | Pool budget multiplied by replicas |
| Jobs | One consumer | Idempotent 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.
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.
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=10sRun 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.

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.
(app replicas × app pool) + (workers × worker pool) + operations + headroom ≤ database limitUse 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.

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
- Build one immutable image and promote the same artifact.
- Apply reviewed backward-compatible migrations once.
- Start new replicas with the same Server Actions key and a new stable deployment ID.
- Wait for readiness and run smoke checks.
- Shift traffic gradually while watching errors, latency, pool waits, and queue age.
- Drain old web and worker processes before termination.
- 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
| Failure | Expected behavior | Test |
|---|---|---|
| One app exits | Readiness removes it; other replicas serve traffic | Terminate one replica under load |
| Redis pauses | Defined cache/session/queue behavior; bounded retries | Inject timeout and reconnect |
| PostgreSQL failover | Pools reconnect; requests remain bounded | Provider-approved failover drill |
| Worker crashes | Unacknowledged job retries without duplicate effect | Exit mid-job |
| Release is bad | Health gates stop rollout; immutable rollback works | Staged canary failure |
| Host is lost | Capacity exists in another failure domain | Controlled 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.
Related Next.js Tutorials
- Blog #14: Performance Optimization
- Blog #19: PostgreSQL and Drizzle ORM
- Blog #20: Docker + Traefik
- Blog #21: Docker Compose
- Blog #22: Redis
- Blog #23: Background Jobs
- Blog #24: Monitoring and Observability
- Blog #26: Security Headers & CSP
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.js self-hosting and multi-server guidance
- Next.js platform requirements
- Next.js Cache Components handlers
- Docker Compose networking
- Docker Compose service reference
- Traefik HTTP load-balancing services
- PostgreSQL connection settings
- Redis key eviction
- Redis persistence
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.
