This article is currently available in English only. Translation coming soon.
You finish an Odoo upgrade — 17.0 to 18.0, or 18.0 to 19.0 — restart workers, and the first request crashes with this in the log:
Traceback (most recent call last):
File "/odoo/odoo/api.py", line 489, in __getitem__
return self.envs[key]
KeyError: 'environments_cache'
Sometimes the variant is KeyError: ('environments_cache', ...) or a similar internal cache key. The error originates inside Odoo's API layer when worker processes carry over state from the previous version's bytecode and class hierarchy. The fix is mechanical, but skipping a step turns a 5-minute reset into a 5-hour outage.
Quick Fix
Stop every Odoo process, clear caches, and restart cleanly:
# 1. Stop all workers
sudo systemctl stop odoo
# 2. Clear Python bytecode and assets
sudo find /opt/odoo -name "__pycache__" -type d -exec rm -rf {} +
sudo find /opt/odoo -name "*.pyc" -delete
# 3. Clear web assets cache (Odoo regenerates on first request)
psql -U odoo -d <db> -c "DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';"
# 4. Restart with --update=all on the upgrade target
sudo -u odoo /opt/odoo/odoo-bin -c /etc/odoo/odoo.conf -d <db> -u all --stop-after-init
# 5. Start normally
sudo systemctl start odoo
Ninety percent of KeyError: 'environments_cache' cases clear after these five steps.
Why This Happens
Odoo's ORM keeps a per-cursor Environment cache that maps (uid, context, su) tuples to ORM environments. The internal key changed between versions (notably 16.0 to 17.0, and 18.0 to 19.0). The error fires when:
- Stale
.pycbytecode from the old version is still on disk and a worker imports it instead of the new source. - A long-running worker survived the upgrade restart (gunicorn/longpolling stuck on a request) and is running 17.0 code while talking to an 18.0-migrated database.
- An
ir.attachmentasset baked from the old version is being served and references removed APIs. - The PostgreSQL session is holding a prepared statement from the old schema (rare, but happens with PgBouncer in transaction mode).
- A custom module uses a private API that moved, like
self.env._cacheorself.env.cache.get_special()— the import succeeds but the attribute access blows up.
Step-by-Step Diagnosis
1. Confirm all workers are stopped.
ps -ef | grep odoo-bin | grep -v grep
You should see zero rows. If you see any, kill them with sudo kill -9 <pid>. SystemD's stop sometimes leaves zombie longpolling workers.
2. Check for stale bytecode.
ls /opt/odoo/odoo/__pycache__/api.cpython-* 2>/dev/null
If the file's mtime is older than your upgrade, you have stale bytecode.
3. Inspect the registry. With Odoo stopped:
psql -d <db> -c "SELECT module, state FROM ir_module_module WHERE state IN ('to upgrade', 'to install', 'to remove');"
Any row in a transitional state means the upgrade did not finish. Re-run -u all --stop-after-init until this query returns zero rows.
4. Check the assets cache.
SELECT count(*) FROM ir_attachment WHERE url LIKE '/web/assets/%';
If non-zero after a major version upgrade, those assets are stale. Delete them; Odoo regenerates on the next request.
5. Test with a fresh worker. Start a single instance with --workers=0 (no preforking) and hit the login page. If it works in single-worker mode, your worker pool was carrying stale state.
Permanent Fix
For a clean, repeatable upgrade flow, codify the reset:
#!/usr/bin/env bash
# upgrade_post_steps.sh — run after every Odoo major upgrade
set -euo pipefail
DB=$1
ODOO_DIR=/opt/odoo
ODOO_USER=odoo
systemctl stop odoo
sleep 5 # let longpolling drain
# Kill stragglers
pkill -9 -u "$ODOO_USER" || true
# Clear bytecode in odoo and addons
find "$ODOO_DIR" -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find "$ODOO_DIR" -name "*.pyc" -delete 2>/dev/null || true
# Clear web assets — Odoo regenerates
sudo -u postgres psql -d "$DB" -c \
"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';"
# Re-run -u all to settle the registry
sudo -u "$ODOO_USER" "$ODOO_DIR/odoo-bin" \
-c /etc/odoo/odoo.conf -d "$DB" -u all --stop-after-init --no-http
systemctl start odoo
Run this script as the last step of every major upgrade. ECOSIRE's Odoo migration team ships a hardened version of this with every customer migration.
For the third-party-module variant of the bug (cause 5 above), the durable fix is in code:
# WRONG — uses private API that moved between versions
val = self.env.cache.get_special(self, 'partner_id')
# RIGHT — use the public API; survives major version upgrades
val = self.partner_id # ORM handles caching internally
How to Prevent It
- Always run
-u all --stop-after-initafter a major version upgrade, not just-u <module>. Module-scoped upgrades skip the global registry rebuild that flushes cross-module caches. - Bake the bytecode wipe into your deploy script. Stale
.pycis the root cause of more upgrade incidents than any other single factor. - Use systemd's
KillMode=mixedin your odoo.service to ensure all child workers die when the unit stops. The default leaves workers running. - Monitor
ir.attachmentasset count post-upgrade. A sudden 10x increase means the new version is being served alongside the old assets — clean them out. - Keep your custom modules off private APIs. Anything starting with
_(likeenv._cache) can move between versions without notice. Stick to documented APIs and your modules survive every major upgrade. - Test the upgrade on a restored production copy first. Always. Every time. The cache reset that solves staging may need a different sequence on production with a different worker count and asset volume.
Related Errors
- Upgrade aborted, database corrupted — bigger sibling failure during the upgrade itself.
- Module not loadable with traceback — what happens when stale bytecode meets new dependencies.
- Server vs modules version mismatch — checks the same registry state but earlier in the boot.
- Model state corruption after upgrade — when the registry itself ends up wrong.
Frequently Asked Questions
Why does the error not appear immediately, only on the second or third request?
Odoo's worker pool spins up lazily. The first few requests hit fresh workers that load the new code; later requests can land on workers that imported stale bytecode at process start, before your reset finished. Restart with --workers=0 once after the upgrade to flush, then return to multi-worker mode.
Should I drop and re-import the database?
No. The database is fine — it migrated cleanly. The error lives in the Python process state, not on disk. A drop-and-restore is the wrong fix and costs you hours of downtime for nothing.
Does this happen on point releases (17.0 → 17.0+)?
Rarely. Patch-level updates do not change the cache key shape, so bytecode reuse is safe. The error is almost exclusively a major-version upgrade artifact.
My PgBouncer is in transaction mode — does it cause this?
It can. PgBouncer in transaction mode pools sessions, and a session can carry server-side prepared statements from before the upgrade. After every major Odoo upgrade, restart PgBouncer along with Odoo. Make this part of the deploy script.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
تحریر
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP کے ساتھ اپنے کاروبار کو تبدیل کریں
آپ کے کاموں کو ہموار کرنے کے لیے ماہر Odoo کا نفاذ، حسب ضرورت، اور معاونت۔
متعلقہ مضامین
How to Add a Custom Button to an Odoo Form View (2026)
Add custom action buttons to Odoo 19 form views: Python action method, view inheritance, conditional visibility, confirmation dialogs. Production-tested.
How to Add a Custom Field in Odoo Without Studio (2026)
Add custom fields via custom module in Odoo 19: model inheritance, view extension, computed fields, store/non-store decisions. Code-first, version-controlled.
How to Add a Custom Report in Odoo Using External Layout
Build a branded PDF report in Odoo 19 using web.external_layout: QWeb template, paperformat, action binding. With print logo + footer overrides.