When to Scale Horizontally
A single Odoo server handles 50-200 concurrent users depending on workload. Beyond that, you hit CPU limits, worker exhaustion, and slow response times. Horizontal scaling adds more Odoo servers behind a load balancer, distributing the load across machines.
Signs you need horizontal scaling:
- CPU consistently above 80% during business hours
- Response times exceeding 3-5 seconds
- Workers frequently reaching memory limits and recycling
- Users reporting timeouts during peak periods
- Large report generation blocking other users
Architecture Overview
Users → Load Balancer (nginx/HAProxy)
↓
┌─────────┼─────────┐
↓ ↓ ↓
Odoo 1 Odoo 2 Odoo 3
(workers) (workers) (workers)
└─────────┼─────────┘
↓
PostgreSQL Primary
↓
PostgreSQL Replica (read-only)Component Architecture
| Component | Role | Scaling |
|---|---|---|
| Load Balancer | Distribute requests, SSL termination | Single (redundant pair) |
| Odoo Workers | Process HTTP requests | Horizontal (2-10+ servers) |
| PostgreSQL Primary | Read/write database | Vertical (bigger server) |
| PostgreSQL Replica | Read-only queries, backups | Horizontal (1-3 replicas) |
| Shared Filestore | Odoo attachments, images | NFS, S3, or GlusterFS |
| Redis/Memcached | Session storage | Single or sentinel |
Step 1: Load Balancer Setup
# nginx load balancer configuration
upstream odoo_backend {
ip_hash; # Session sticky by client IP
server 10.0.1.10:8069;
server 10.0.1.11:8069;
server 10.0.1.12:8069;
}
upstream odoo_longpolling {
ip_hash;
server 10.0.1.10:8072;
server 10.0.1.11:8072;
server 10.0.1.12:8072;
}
server {
listen 443 ssl;
server_name erp.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/erp.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/erp.yourdomain.com/privkey.pem;
proxy_read_timeout 720s;
proxy_connect_timeout 720s;
proxy_send_timeout 720s;
location /longpolling {
proxy_pass http://odoo_longpolling;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
proxy_pass http://odoo_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Step 2: Shared Filestore
All Odoo servers must share the same filestore. Options:
NFS (Simplest)
# On filestore server
apt install nfs-kernel-server
mkdir -p /srv/odoo-filestore
chown odoo:odoo /srv/odoo-filestore
echo '/srv/odoo-filestore 10.0.1.0/24(rw,sync,no_subtree_check)' >> /etc/exports
exportfs -ra
# On each Odoo server
apt install nfs-common
mount -t nfs fileserver:/srv/odoo-filestore /opt/odoo/data/filestoreS3-Compatible Object Storage
Use an S3 adapter for Odoo to store attachments in object storage. This eliminates the shared filesystem requirement entirely and provides unlimited, durable storage.
Step 3: Session Management
By default, Odoo stores sessions on the local filesystem. With multiple servers, you need centralized session storage:
# Option 1: Sticky sessions (ip_hash in nginx)
# Simplest but not fault-tolerant. If a server dies, those users lose sessions.
# Option 2: PostgreSQL session store
# Odoo can store sessions in the database (slower but shared automatically)
# Option 3: Redis session store (recommended)
# Requires custom session handler or community module
# Redis provides fast, shared session storage across all Odoo serversStep 4: Database Replication
For read-heavy workloads, set up PostgreSQL streaming replication:
# On primary
postgresql.conf:
wal_level = replica
max_wal_senders = 5
wal_keep_size = 1024
# On replica
pg_basebackup -h primary_host -D /var/lib/postgresql/data -U replicator -P
postgresql.conf:
primary_conninfo = 'host=primary_host user=replicator password=xxx'
hot_standby = onNote: Odoo does not natively support read replicas for query routing. You would need a connection proxy like PgBouncer with read/write splitting, or a custom Odoo patch to route specific queries (reports, analytics) to the replica.
Step 5: Worker Configuration
With multiple servers, tune workers per server:
# Per Odoo server (4 vCPU, 8GB RAM)
workers = 6
max_cron_threads = 1 # Only ONE server should run cron!
limit_memory_hard = 2684354560
limit_memory_soft = 2147483648
limit_time_cpu = 600
limit_time_real = 1200Critical: Only one Odoo server should run cron jobs. Set max_cron_threads = 0 on all servers except one to prevent duplicate cron execution.
Step 6: Cron Job Isolation
Designate one server as the cron runner. All other servers handle only HTTP requests:
# Server 1 (cron + HTTP)
workers = 4
max_cron_threads = 2
# Server 2 (HTTP only)
workers = 6
max_cron_threads = 0
# Server 3 (HTTP only)
workers = 6
max_cron_threads = 0Capacity Planning
| Concurrent Users | Odoo Servers | Workers Total | DB Server |
|---|---|---|---|
| 1-50 | 1 | 4-6 | Same server |
| 50-150 | 2 | 8-12 | Dedicated 4 vCPU |
| 150-300 | 3 | 12-18 | Dedicated 8 vCPU |
| 300-500 | 4-5 | 18-30 | Dedicated 16 vCPU + replica |
| 500+ | 5+ | 30+ | Dedicated 32 vCPU + replicas |
DeployMonkey + Horizontal Scaling
DeployMonkey manages multi-server Odoo deployments. Add servers, configure load balancing, and scale your Odoo deployment without managing infrastructure yourself.