Scaling Odoo 19 Enterprise past 100 concurrent users is not a matter of vertically upsizing virtual machine CPU cores—it requires decoupling Odoo's Python worker processes from raw PostgreSQL database connections using PgBouncer in transaction mode. Without connection pooling and aggressive database buffer tuning, Odoo deployments suffer severe connection starvation, memory bloat, and transactional deadlocks during peak morning logins and batch inventory validations.

1. The Concurrency Ceiling: Python GIL & Worker Starvation

Odoo is built on Python. Because of the Global Interpreter Lock (GIL), an Odoo server running in single-threaded mode cannot utilize multi-core CPU architectures. To scale, Odoo runs in multi-process mode (configured via the workers directive in odoo.conf).

In this multi-process architecture, every HTTP request spawns or claims a dedicated Python worker process. By default, every worker process opens and maintains its own persistent connection to PostgreSQL.

When an enterprise scales to 200 users:

  • 30 Odoo worker processes open 30 to 90 idle database connections.
  • PostgreSQL forks a dedicated backend process for every connection, consuming 10MB–20MB of RAM per connection just in socket overhead.
  • During high-velocity operations (e.g., automated barcode scanning in the warehouse or point-of-sale receipt explosions), the database exhausts max_connections, queuing transactions and degrading response times from 150ms to over 8 seconds.

2. Mathematical Worker Sizing & Memory Limits

Never guess your worker count. Configure odoo.conf using verified production formulas based on your hardware profile and user behavior:

# Standard Odoo Worker Calculation Formula: # Total Workers = (Number of Physical CPU Cores * 2) + 1 # # Dedicated Cron Workers = 2 (minimum for enterprise MRP and automated reconciliations) # Dedicated HTTP Workers = Total Workers - Dedicated Cron Workers # Example for an 8-Core / 32GB RAM Server: # Total Workers = (8 * 2) + 1 = 17 workers # HTTP Workers = 15 # Cron Workers = 2

Memory Guardrails in odoo.conf:

Python processes suffer from memory fragmentation during heavy PDF report generation (e.g., printing 500-page picking lists). Configure strict memory boundaries to cycle workers cleanly without crashing the operating system:

[options] limit_time_cpu = 600 limit_time_real = 1200 limit_memory_soft = 2147483648   # 2048 MB (cycles worker after request completes) limit_memory_hard = 2684354560   # 2560 MB (terminates runaway memory leaks instantly) max_cron_threads = 2 workers = 17

3. PgBouncer Architecture & the LISTEN/NOTIFY Split

The definitive solution to connection exhaustion is inserting PgBouncer between Odoo and PostgreSQL. PgBouncer acts as a lightweight proxy, holding thousands of client connections open while multiplexing active SQL queries through a lean pool of 20 to 30 actual PostgreSQL connections.

The Transaction Mode Requirement:

You must set pool_mode = transaction in pgbouncer.ini. In transaction mode, a server connection is allocated only while an active SQL transaction is executing. The moment Odoo issues a COMMIT or ROLLBACK, PgBouncer returns the connection to the pool, allowing other workers to use it.

The Critical Longpolling & Websocket Trap:

Odoo 19 relies on PostgreSQL's native LISTEN/NOTIFY mechanism for real-time web chat, barcode scanner notifications, and activity popups. PgBouncer in transaction mode does not support LISTEN/NOTIFY because notification listeners require persistent session state.

The Production Routing Architecture:

  • Standard HTTP Web Workers (Port 8069): Configured in odoo.conf to connect to PgBouncer on port 6432 (Transaction Pooling).
  • Websocket / Longpolling Workers (Port 8072): Configured to bypass PgBouncer, connecting directly to PostgreSQL on port 5432 with dedicated persistent session connections.
# /etc/pgbouncer/pgbouncer.ini [databases] odoo_production = host=127.0.0.1 port=5432 dbname=odoo_production [pgbouncer] listen_port = 6432 listen_addr = 0.0.0.0 auth_type = md5 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction default_pool_size = 25 min_pool_size = 10 reserve_pool_size = 5 max_client_conn = 1000 server_idle_timeout = 60

4. Production PostgreSQL Kernel Tuning for Odoo 19

Default PostgreSQL configuration settings are tuned for 1990s hardware with 512MB of RAM. An enterprise Odoo database hosting millions of account_move_line and stock_quant rows requires dedicated database kernel tuning:

Configuration ParameterRecommended Production ValueArchitectural Impact on Odoo
shared_buffers25% of total system RAM (e.g., 8GB on a 32GB host)Allocates dedicated memory for caching frequently queried ERP index trees and master data.
effective_cache_size75% of total system RAM (e.g., 24GB on a 32GB host)Informs the query planner how much data is cached in the OS buffer cache, encouraging fast index scans.
work_mem64MB – 128MB per connectionAllows complex ledger aggregations and inventory sorting in memory, preventing temporary disk writes.
maintenance_work_mem2048 MBAccelerates VACUUM and GIN/B-tree index creation during nightly maintenance windows.
checkpoint_completion_target0.9Spreads dirty disk writes across 90% of the checkpoint duration, smoothing I/O throughput.
max_wal_size16 GBPrevents premature checkpoints during bulk inventory imports and fiscal year-end closings.

5. High-Availability AWS Architecture (ECS, RDS & EFS)

For enterprise production exceeding 250 users, deploying on a single monolithic EC2 virtual machine introduces a single point of failure. Deploy a decoupled, cloud-native architecture on AWS:

  1. Application Tier (AWS ECS Fargate): Run containerized Odoo web instances across multiple Availability Zones behind an Application Load Balancer (ALB). Autoscaling policies scale tasks based on average CPU and target response time.
  2. Dedicated Background Worker Task: Run a separate, non-scaling ECS task with max_cron_threads = 4 dedicated solely to background jobs. This guarantees that automated MRP runs, bank synchronizations, and subscription renewals never consume CPU cycles from front-office sales and warehouse staff.
  3. Database Tier (Amazon RDS PostgreSQL Multi-AZ): Deploy a provisioned RDS instance with gp3 storage (minimum 3,000 IOPS and 250 MB/s throughput). Multi-AZ replication guarantees synchronous failover within 60 seconds with zero data loss.
  4. Shared Filestore (AWS EFS): Mount the Odoo filestore (/var/lib/odoo/filestore) onto Amazon Elastic File System (EFS) with Provisioned Throughput to eliminate NFS file lock contention.

6. FinOps Evaluation: Odoo.sh vs. AWS Dedicated Infrastructure

Odoo.sh is a managed platform ideal for standard implementations. However, as enterprise estates grow, its pricing model scales on dedicated worker allocations:

MetricOdoo.sh Dedicated TierAWS Cloud-Native (ECS + RDS Multi-AZ)
Concurrency Sizing16 Dedicated Workers + 2 Staging branchesAutoscaling 4–8 ECS Fargate tasks (32+ workers)
Database TuningStandard managed configuration; no custom kernel parametersFull control over postgresql.conf, PgBouncer, and custom extensions
Storage ThroughputShared multi-tenant storage bandwidthDedicated 3,000–6,000 IOPS provisioned on RDS gp3
Estimated Monthly Run-Rate~$3,800 – $4,600 / mo~$1,950 – $2,400 / mo

The Architectural Recommendation: Use Odoo.sh during development and initial implementation for its continuous integration and branch-preview tooling. Once concurrent users exceed 150 and custom heavy integrations (such as continuous EDI or IoT warehouse automation) are live, migrate production to dedicated AWS ECS and RDS infrastructure.