# Session Management in Odoo, Part 3: The FAQ

> Short answers to the session questions we get most in production: expiry, force-logout, storage, clustering, and why an integration script loses its session overnight.

**Date:** 2026-07-05
**Source:** <https://trobz.com/insights/odoo-session-faq/>

---



[Part 1]({{< surl >}}/insights/odoo-session-internals) and [Part 2]({{< surl >}}/insights/odoo-session-modules) of this series covered how Odoo sessions work internally and which community modules extend them. This closing post takes the opposite angle: the questions we actually get asked, with short answers and pointers back to the detailed explanations. All answers apply to Odoo 19.0 unless a version is stated; most hold for 16.0 and later.

---

## Lifetime and Expiry

### How Long Does an Odoo Session Last?

Forever, as long as the user stays active. Odoo only expires sessions on *inactivity*: each request slides the deadline forward, so a session in daily use never dies. The default inactivity threshold is seven days since Odoo 16.0. Before 16.0 the numbers were inconsistent: the cookie was valid for 90 days but the server deleted inactive session files after 7, so 7 days was already the effective limit. Odoo 16.0 aligned both on 7 days [1].

There is no absolute lifetime cap — no native setting says "force re-login every N days regardless of activity." If a compliance policy requires one, it takes custom code (covered further down in this section).

### How Do I Change the Session Inactivity Threshold?

Set the `sessions.max_inactivity_seconds` system parameter (Settings → Technical → System Parameters). It drives both the cookie `Max-Age` and the server-side cleanup threshold [2]. Two caveats:

- Expired files are only deleted when the daily "Base: Auto-vacuum internal data" cron runs. If you set a short value, increase that cron's frequency to match.
- Odoo does not check session age at request time. A browser drops the cookie when it expires, but a script that keeps the cookie can use the session until the cron deletes the file.

### Is It Possible to Cap the Maximum Lifetime of a Session, Regardless of Activity?

Not natively. The only way to enforce a hard cap — "force re-login every 24 hours no matter what" — is a small custom module: stamp the session with the real login time at authentication, then check it on every request and force a logout once the cap is exceeded.

### I Set a Short Timeout but Users Stay Logged in. Why?

Most likely the auto-vacuum cron has not run yet (see above), or the users are active: the timeout counts inactivity, not total session age. If you need a hard "log out after N minutes idle" behaviour enforced on the very next request, use the OCA module `auth_session_timeout` instead — it checks inactivity per request rather than waiting for the cron (see [Part 2]({{< surl >}}/insights/odoo-session-modules)).

### Why Did My Session ID Change in the Middle of the Day?

That is soft rotation: every three hours of activity, Odoo regenerates the last half of the session ID while keeping the first 42 bytes stable [3]. It is a security refresh, invisible to the user, and CSRF tokens survive it precisely because the prefix is preserved.

### Do POS Sessions Expire With These Settings?

No — different concept entirely. A POS session is a business object (`pos.session`) that tracks a cash register's opening and closing; it is unrelated to the HTTP sessions discussed in this series.

---

## Logout and Invalidation

### How Do I Force-Logout One User?

Change their password or deactivate their account. Every session stores a `session_token`, an HMAC computed over the user's login, hashed password, and `active` flag; on each request Odoo recomputes it from the live database, so any change to those inputs invalidates all of the user's sessions immediately [4]. No cache flush, no server restart.

For remote force-logout via an API endpoint, without touching the password, watch the `auth_session_logout_api` module Trobz contributed to OCA/server-auth (pending merge, see [Part 2]({{< surl >}}/insights/odoo-session-modules)).

### How Do I Force-Logout Everyone at Once?

Rotate the `database.secret` system parameter. It is the root key of every session token, so changing it invalidates all sessions for all users in the database instantly [4]. It is the emergency lever for a suspected compromise; expect every user to land back on the login page.

### Does Restarting Odoo Log Users Out?

No. Sessions live on disk (or in PostgreSQL/Redis with the modules from Part 2), not in server memory. A restart, an upgrade, or a worker recycle does not touch them.

### Can I See Which Devices or IPs Are Logged in for a User?

Partially. Odoo tracks a device log per session in `res.device.log`: platform, browser, IP, and first/last activity timestamps, referenced in Part 1's "Connected devices" feature [7]. It is populated automatically on authenticated requests and is queryable like any other model, which makes it useful for security audits. What it does not give you out of the box is a self-service "log out this device" button — the `revoked` flag on each record is bookkeeping the framework updates itself when a session disappears, not a lever you flip to terminate one. To actually force a specific session out, fall back to the force-logout answer above (password change, or the pending `auth_session_logout_api` for surgical, password-free logout).

---

## Storage and Operations

### Where Are Session Files and Can I Delete Them?

`{data-dir}/sessions/`, scattered across up to 4096 two-character subdirectories, one JSON file per session [5]. Deleting a file is safe and simply logs that session out — deleting all of them is the crude version of rotating `database.secret`. Since 16.0 you should not need manual cleanup: the auto-vacuum cron reaps expired files daily.

### My Sessions Directory Keeps Growing. Is That Normal?

Some growth is normal: every visitor, health check, and API client that becomes "dirty" gets a session file. Files older than the inactivity threshold should be reaped daily by the auto-vacuum cron. If they are not, check that the cron is running, and check the `ODOO_SKIP_GC_SESSIONS` environment variable — when set, Odoo skips its built-in session GC entirely and expects something else to clean up [6].

### Users Get Randomly Logged Out Since We Added a Second Server. What Happened?

The filesystem session store is per-machine: a session created on node A does not exist on node B, so requests landing on the wrong node hit the login page. You need a shared store — `session_db` (PostgreSQL) or `session_redis` — both covered in [Part 2]({{< surl >}}/insights/odoo-session-modules). This is the single most common session issue we see in clustered deployments.

### Can Sticky Sessions Replace `session_db` or `session_redis`?

Sticky sessions (routing a client to the same node via `ip_hash` or a reverse-proxy cookie) work around the filesystem store being per-machine without introducing a shared store. They avoid new infrastructure, but they trade away the reason you added a second node in the first place: a node failure or a deploy that recycles workers drops every session pinned to it, and load stops being evenly distributed once some clients stick to a busy node. `session_db` or `session_redis` remove the pinning requirement entirely, at the cost of one more dependency. For anything beyond a small cluster, prefer the shared store.

### Can I Read a Session File to Debug an Issue?

Yes: it is plain JSON. You will find `uid`, `db`, `login`, the user's `context`, the `session_token`, debug flags, and device-trace data. [Part 1]({{< surl >}}/insights/odoo-session-internals) documents every field. Do not edit files by hand — the `session_token` binds the content to database state, and a mismatch invalidates the session.

---

## Security

### Is the Session Cookie Enough to Hijack an Account?

A stolen `session_id` cookie does grant access while the session is valid — which is why the cookie is `HttpOnly` (unreadable from JavaScript) and why sessions hard-rotate at login and logout. The mitigation lever is the `session_token`: changing the victim's password immediately cuts off the attacker, because the recomputed token no longer matches [4].

### Are Sessions Shared Between Databases?

No. Each session is bound to exactly one database via its `db` field, and the `session_token` is keyed with that database's own `database.secret`. On a multi-database server, logging into a second database replaces the session.

### Can I Embed a Logged-In Odoo Page Inside an Iframe on Another Site?

Not the backend: Odoo sends `X-Frame-Options: SAMEORIGIN` on backend pages by design, so browsers refuse to render them in a cross-site frame at all. For pages that are meant to be embeddable (portal or website pages), you would still hit the cookie itself: `session_id` is set without an explicit `SameSite` attribute [8], which browsers default to `Lax` — a `Lax` cookie is not sent on cross-site requests initiated by another site's iframe. Making a session cookie work cross-site would require deliberately setting `SameSite=None; Secure` on it, which Odoo does not do for the authentication cookie, since it would weaken CSRF protection for the *whole* session. If you need cross-site embedding, look at scoping the exposed page to something that does not rely on `session_id` (a signed one-time token, a public/portal token) rather than trying to relax the cookie.

---

## Integrations and Automation

### My Integration Script Authenticates Once, Then Gets "Session Expired" Hours Later. What's Happening?

The script's session is subject to the same inactivity rule as a browser's: if the script doesn't touch the session for longer than `sessions.max_inactivity_seconds` — most likely because it holds the cookie but doesn't send further authenticated requests during a long processing gap, or because the auto-vacuum cron reaped the file — the next call fails. This is not a JSON-RPC or XML-RPC quirk; it's the same server-side expiry covered above, just hit by a client that does not retry.

The fix is the same pattern you'd use for any short-lived token: don't assume one `/web/session/authenticate` call is good forever. For long-running jobs, either re-authenticate on a `session expired` response, or check session validity before a batch (e.g. a lightweight authenticated call) and re-authenticate proactively if it fails, rather than discovering it mid-run.

---

## Key Takeaways

Odoo sessions do not expire on a clock — they expire on inactivity, which means an actively used session can live indefinitely, including for integration scripts that need to re-authenticate on long jobs. The inactivity threshold is one system parameter (`sessions.max_inactivity_seconds`), remembering that cleanup is enforced by a daily cron rather than at request time. An absolute lifetime cap, independent of activity, requires custom code. Force-logout is a password change (one user) or a `database.secret` rotation (everyone); `res.device.log` gives visibility into active sessions but not a one-click way to kill one. Restarts never log users out; adding a second node without a shared session store always does, and sticky sessions only paper over that without fully solving it.

**Back to the series:** [Part 1]({{< surl >}}/insights/odoo-session-internals) covers the session internals in depth; [Part 2]({{< surl >}}/insights/odoo-session-modules) covers `session_db`, `session_redis`, and lifecycle modules.

---

## Sources

[1] `odoo/http.py:307-308` (Odoo 19.0, `SESSION_LIFETIME`, 7 days) and odoo/odoo commit `cea9150fb7` ("make session lifetime consistent and configurable", 16.0)

[2] `odoo/http.py:452-461`: `get_session_max_inactivity()`

[3] `odoo/http.py:1008-1040`: `rotate()` (soft/hard rotation)

[4] `odoo/addons/base/models/res_users.py:851-884`: `_compute_session_token()`

[5] `odoo/http.py:968-1006`: `FilesystemSessionStore`

[6] `odoo/addons/base/models/ir_http.py:406-410`: `_gc_sessions()` autovacuum cron, `ODOO_SKIP_GC_SESSIONS`

[7] `odoo/addons/base/models/res_device.py:17-38, 84-165`: `res.device.log` fields, `update_trace()`, revocation bookkeeping

[8] `odoo/http.py:1757-1763`: `Response.set_cookie()` — no explicit `SameSite` passed for `session_id`, so it falls back to the browser default (`Lax`)

