How to Perform a Technical Audit of Your Odoo Production Instance

Complete checklist to audit performance, security, backups, custom modules, technical debt and infrastructure of an Odoo production instance. With real metrics and deliverable.

Why your Odoo production instance needs a periodic audit

The vast majority of Odoo production instances in Spain have been running for months or years without anyone having carried out an in-depth review of whether the configuration is still appropriate. The system keeps responding, users keep working, and the feeling is that it "works". Until it doesn't.

The problem is that the deterioration of an Odoo instance is gradual and invisible. Queries that took 200 ms two years ago now take 3 seconds because the account_move table has grown from 50,000 to 800,000 records and the indices have not been reviewed. The backups that were set up at launch have gone six months without being verified and the last valid backup is four days old. Twelve third-party modules have been installed without a code audit, some with an unnecessary sudo in an API controller. The Odoo workers are configured for the original 4-core server, which now has 16 cores.

A technical audit is not a months-long project. It is a systematic review of between 4 and 8 hours of expert work that produces a prioritised findings report and an action plan. This article describes exactly how to conduct one.

Block 1: Performance and worker configuration

Odoo's performance depends on three variables that must be aligned: server resources, the worker configuration in odoo.conf, and database behaviour.

1.1 Verify the worker configuration

The Odoo reference formula for sizing workers is:

# workers = (cores * 2) + 1  (recomendación general)
# max_cron_threads = 1 o 2 (nunca 0 en producción)
# limit_memory_hard <= RAM_total / (workers + 2)

# Verificar configuración actual:
cat /etc/odoo/odoo.conf | grep -E 'workers|memory|cpu|limit|thread'

# Ver workers activos en tiempo real:
ps aux | grep odoo | grep -v grep | wc -l

# Ver uso de memoria por proceso:
ps aux --sort=-%mem | grep odoo | head -20

On an instance with 8 cores and 32 GB of RAM, the correct configuration is approximately: workers = 16, limit_memory_hard = 2684354560 (2.5 GB), limit_memory_soft = 2147483648 (2 GB). If the server has those resources and workers = 4 (the default configuration for many installations), 75% of the server's capacity is being left unused.

1.2 Detect slow queries in PostgreSQL

Enable log_min_duration_statement if not already active and analyse with pg_stat_statements:

-- Verificar si pg_stat_statements está activo:
SELECT * FROM pg_extension WHERE extname = 'pg_stat_statements';

-- Si no está, activar (requiere reinicio de PG):
-- shared_preload_libraries = 'pg_stat_statements'  en postgresql.conf
CREATE EXTENSION pg_stat_statements;

-- Top 15 queries más lentas (tiempo total acumulado):
SELECT
    LEFT(query, 120)               AS query_preview,
    calls,
    ROUND(total_exec_time::numeric, 2) AS total_ms,
    ROUND(mean_exec_time::numeric, 2)  AS mean_ms,
    ROUND(stddev_exec_time::numeric, 2) AS stddev_ms,
    rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 15;

-- Top queries por llamadas (candidates para optimizar con índices):
SELECT
    LEFT(query, 120) AS query_preview,
    calls,
    ROUND(mean_exec_time::numeric, 2) AS mean_ms
FROM pg_stat_statements
ORDER BY calls DESC
LIMIT 15;

1.3 Missing indices and bloated tables

-- Tablas más grandes (candidatas a revisar vacuum/autovacuum):
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,
    n_dead_tup,
    n_live_tup,
    ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct,
    last_autovacuum,
    last_autoanalyze
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;

-- Índices no usados (candidatos a eliminar):
SELECT
    schemaname,
    tablename,
    indexname,
    idx_scan,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
  AND schemaname = 'public'
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;

Block 2: Security

Security of an Odoo production instance has four layers to audit: the server, Nginx, the Odoo configuration, and the installed custom modules.

2.1 Server security checklist

  • Is port 8069 (Odoo) directly accessible from the internet? It must be blocked by the firewall; only Nginx should be able to connect to it.
  • Is port 5432 (PostgreSQL) accessible from the internet? It must be closed. Only the application server should be able to connect.
  • Are the system packages up to date? apt list --upgradable 2>/dev/null | wc -l — if there are more than 20 packages pending update, review them.
  • Are there users with default SSH keys or weak passwords? Audit /etc/ssh/sshd_config: PasswordAuthentication no must be active.
  • Is fail2ban configured for the SSH port and for Nginx? IPs that brute-force should be banned automatically.

2.2 Odoo security checklist

# Verificar que el master password de Odoo está configurado y es robusto:
grep admin_passwd /etc/odoo/odoo.conf
# Si está vacío o es "admin", CRÍTICO — cualquiera puede acceder a /web/database/manager

# Verificar que el gestor de BBDD está bloqueado o deshabilitado:
curl -s https://tuodoo.com/web/database/manager | grep -c "Database Manager"
# Si devuelve 1, el gestor está expuesto. Añadir: list_db = False en odoo.conf

# Verificar headers de seguridad en Nginx:
curl -sI https://tuodoo.com | grep -E 'X-Frame|X-Content|Strict|Content-Security'

2.3 Quick audit of custom modules: risk patterns

Custom modules are the most common attack surface in mature Odoo instances. Review with grep:

# Buscar uso de sudo() sin justificación en controladores de API:
grep -r 'sudo()' /opt/odoo/custom_addons/ --include='*.py' -l

# Buscar queries SQL construidas por concatenación (riesgo SQLi):
grep -rn "cr.execute.*%s" /opt/odoo/custom_addons/ --include='*.py' | grep -v 'params'

# Buscar eval() o exec() (riesgo de ejecución de código arbitrario):
grep -rn 'eval(\|exec(' /opt/odoo/custom_addons/ --include='*.py'

# Buscar rutas HTTP sin autenticación declarada:
grep -rn "auth.*public\|auth.*none" /opt/odoo/custom_addons/ --include='*.py'

Block 3: Backups

In half of the audits we carry out, the backup "works" in theory but nobody has ever restored it to verify it. An unverified backup is not a backup: it is wishful thinking.

3.1 Verify that backups exist and are recent

# Verificar backups de PostgreSQL (pg_dump o pgBackRest):
ls -lhrt /var/backups/odoo/ | tail -10
# ¿Cuándo fue el último? ¿Cuántos días de antigüedad tiene?

# Si se usa el backup nativo de Odoo (zip de BBDD + filestore):
ls -lhrt /var/lib/odoo/backups/ | tail -10

# Tamaño del backup vs. tamaño de la BBDD (deben ser comparables):
du -sh /var/backups/odoo/latest_backup.zip
psql -U odoo -c "SELECT pg_size_pretty(pg_database_size('odoo_prod'));"

3.2 Restore test (the step nobody does)

# Restaurar el último backup en una BBDD temporal para verificar integridad:
pg_restore -U postgres -d odoo_test_restore \
  --no-owner --no-acl \
  /var/backups/odoo/backup_$(date +%Y%m%d).dump

# Verificar que la restauración tiene las tablas esperadas:
psql -U odoo -d odoo_test_restore -c "
SELECT COUNT(*) FROM sale_order;
SELECT COUNT(*) FROM account_move;
SELECT MAX(write_date) FROM res_partner;
"
# Si los conteos son cero o la fecha es antigua, el backup está corrupto

Block 4: Custom modules vs. core — technical debt

One of the biggest long-term risks of an Odoo instance is the accumulation of custom modules that override core behaviour without documentation or tests. Every Odoo version upgrade becomes a months-long project if there are dozens of custom modules with extensive overrides.

4.1 Inventory of installed modules

-- Módulos instalados que NO son de Odoo ni de OCA:
SELECT
    name,
    author,
    installed_version,
    state,
    category
FROM ir_module_module
WHERE state = 'installed'
  AND author NOT ILIKE '%Odoo%'
  AND author NOT ILIKE '%OCA%'
ORDER BY name;

-- Módulos con muchos modelos heredados (señal de deuda técnica alta):
SELECT
    module,
    COUNT(*) AS inherited_models
FROM ir_model_inherit
GROUP BY module
ORDER BY inherited_models DESC
LIMIT 20;

4.2 Classifying technical debt

Type of debtSignalRisk
Core method overrides without correct inheritancedef write(self): without super()High — breaks on upgrades
Inherited views with fragile xpath//field[@name='x'] in views that change across versionsMedium
Configuration data in Python codeHardcoded IDs in code (env.ref('module.record_id'))Medium — environment-dependent
Modules without testsEmpty or missing tests/ directoryMedium — silent risk on upgrades
Third-party module dependencies without maintenanceLast commit >2 years ago on GitHubHigh for migrations

Block 5: Infrastructure and operating system

  • Operating system version: Ubuntu 22.04 LTS or Debian 12 are the recommended platforms in 2026. If the server runs Ubuntu 18.04 or 20.04, the support cycle is ending or has already ended.
  • PostgreSQL version: Odoo 16/17 with PostgreSQL 14 or 15 is correct. PostgreSQL 12 or earlier has a less efficient query planner and outdated security extensions.
  • Disk space: verify that at least 30% of disk space is free. A full disk on an Odoo production server causes data corruption.
  • Swap: must exist and be at least equal to the RAM. If the OOM killer is active in the logs, the Odoo memory configuration (limit_memory_hard) is incorrectly sized.
# Verificaciones de infraestructura en un comando:
echo "=== DISCO ==="
df -h | grep -v tmpfs
echo "=== MEMORIA ==="
free -h
echo "=== SWAP ==="
swapon --show
echo "=== OOM KILLS (últimas 48h) ==="
dmesg --since -48h | grep -i 'oom\|killed process' | tail -10
echo "=== VERSIÓN PG ==="
psql --version
echo "=== UPTIME ==="
uptime
echo "=== CARGA SISTEMA ==="
sar -u 1 3 2>/dev/null || top -bn1 | head -5

Block 6: Observability — what metrics to monitor

An Odoo instance without observability is a system that only warns you when it is already broken. The minimum metrics that must be monitored are:

MetricAlert thresholdHow to measure
TTFB (Time To First Byte) of Odoo> 2 s on critical pathsBlackbox exporter / curl + time
Active workers vs. available> 80% of workers busyEndpoint /web/health + Nginx metrics
Active PostgreSQL connections> 80% of max_connectionsSELECT count(*) FROM pg_stat_activity
Replication lag (if replica exists)> 30 secondspg_stat_replication
Blocked queries (locks)Any query > 5 min blockedpg_stat_activity WHERE wait_event_type='Lock'
Disk space> 70% usednode_exporter / df
Backup successMore than 24 h without a valid backupVerification script + Telegram alert

The fastest tool to deploy for Odoo is the ELK stack (Elasticsearch, Logstash, Kibana) with Filebeat collecting Odoo logs, combined with alerts via a Telegram bot. With that setup, any 500 error, slow query or worker failure generates a message to the technical lead's phone in real time.

The deliverable of a technical audit

A professional technical audit does not end with an unprioritised list of findings. The deliverable must have this structure:

  1. Executive summary (1 page): the 3–5 critical risks requiring immediate action, in non-technical language for management.
  2. Findings by block: performance, security, backups, code, infrastructure, observability. Each finding with severity (critical / high / medium / low), concrete evidence and a specific recommendation.
  3. Prioritised action plan: what to do first, what can wait, what is technical debt to be managed in the medium term.
  4. Effort estimate: approximate hours to resolve each finding, so that the client can plan and budget.
  5. Metrics baseline: snapshot of the key metrics at the time of the audit (slow queries, disk space, worker configuration) for comparison in future reviews.

How often to audit

The general recommendation depends on system usage:

  • Critical instance (> 50 users, real-time operations): quarterly metrics review + full annual audit.
  • Medium instance (10–50 users, office operations): full audit every 6 months.
  • Small instance (< 10 users): annual audit, plus always before an Odoo version upgrade.
  • After any serious incident (outage, data corruption, security incident): immediate audit.

What an audit cannot do on its own

An audit is a snapshot of the current state. Without an executed remediation plan, the findings pile up in a document that nobody re-reads. The real value lies in the combination of diagnosis + action plan + execution. For critical instances, the recommendation is to retain a monthly technical retainer that includes the proactive resolution of recurring findings: query optimisation, update management, backup verification and continuous monitoring.

Do you want us to review your Odoo in production?

Request a free technical audit

Complete CI/CD Pipeline for Odoo with GitHub Actions and Docker
Real architecture of a continuous integration and deployment pipeline for Odoo modules in production