Session Garbage Collection
Last updated Oct 10th, 2026 | Page history | Improve this page | Report an issue
Support the team building MODX with a monthly donation.
The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.
Backers
Budget
$194 per month—let's make that $500!
Learn moreBy default Revolution stores sessions in the database with MODX\Revolution\modSessionHandler (table {table_prefix}session, often modx_session). PHP must run session garbage collection (GC) on that handler, or expired rows stay forever.
On many hosts, especially Debian and Ubuntu, session.gc_probability is 0 because the OS cleans file sessions with a cron job. That cron does not touch the MODX session table. SiteDash data around 2021 suggested roughly three in ten MODX sites had no working GC. Tables of several gigabytes and dead sites are a known outcome (modxcms/revolution#16275).
Setup in current Revolution releases warns when GC probability is zero (modxcms/revolution#16448). Existing installs still need a manual check.
How GC runs¶
On a share of requests, PHP calls the session handler's gc() method. The chance is session.gc_probability / session.gc_divisor: with 1 / 100, about 1% of requests run GC.
modSessionHandler::gc() deletes session rows whose access timestamp is older than session_gc_maxlifetime (seconds). Default is 604800 (seven days). That setting also drives session.gc_maxlifetime when MODX starts the session.
If session.gc_probability is 0, gc() never runs. New visits keep inserting rows. The table only grows.
Check your server¶
In SSH or a one-off PHP script:
php -i | grep -E 'session.gc_probability|session.gc_divisor|session.gc_maxlifetime'
Or create a temporary PHP file:
<?php
header('Content-Type: text/plain');
foreach (['session.gc_probability', 'session.gc_divisor', 'session.gc_maxlifetime'] as $key) {
echo $key, '=', ini_get($key), PHP_EOL;
}
Remove the file after you read the values.
In MySQL, watch table size and row count:
SHOW TABLE STATUS LIKE 'modx_session';
SELECT COUNT(*) FROM modx_session;
Use your real table prefix if it is not modx_.
Fix it: enable PHP session GC¶
Prefer a lasting php.ini (or pool) change so every request sees the same values:
session.gc_probability = 1
session.gc_divisor = 100
1 / 100 is a common starting point. Raise probability only if the table still grows under heavy traffic.
If you cannot edit the main php.ini:
- Some hosts allow
.user.iniin the web root with the same keys. - Apache with
AllowOverridemay acceptphp_value session.gc_probability 1in.htaccess. - PHP-FPM: set the values in the pool config and reload PHP.
Confirm with php -i (CLI) and a web ini_get() script. CLI and the web SAPI often load different ini files.
After GC runs again, old rows drop according to session_gc_maxlifetime. Align that setting with session_cookie_lifetime so cookies and DB cleanup agree.
Fix it: scheduled cleanup when you cannot change php.ini¶
If the host locks session.gc_probability at 0, schedule your own cleanup.
Option A — cron that boots MODX and calls GC (adjust paths):
# Daily example
0 3 * * * php /path/to/public_html/core/components/your-tool/session-gc.php
A minimal CLI script that loads MODX and runs handler GC (put it outside the web root when you can):
<?php
define('MODX_API_MODE', true);
require '/path/to/config.core.php';
require MODX_CORE_PATH . 'vendor/autoload.php';
$modx = \MODX\Revolution\modX::getInstance();
$modx->initialize('web');
$handler = new \MODX\Revolution\modSessionHandler($modx);
$handler->gc(0); // the argument is ignored: the cutoff is always session_gc_maxlifetime
modSessionHandler::gc($max) does not use its $max parameter — rows are deleted by the session_gc_maxlifetime setting only (so any integer works, e.g. 0).
Prefer a small extra or a documented project script over ad-hoc files in the web root.
Option B — SQL cron for expired rows (match session_gc_maxlifetime, default seven days):
DELETE FROM modx_session
WHERE access < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY));
Run this only when you understand the lifetime you want. Wrong intervals log users out early.
Emergency: clear all sessions¶
User menu (top right) → Access → Logout All Users (permission flush_sessions) truncates the session table via modSessionHandler::flushSessions(). Everyone is logged out, including you.
Use it when the table is already huge and you need space now, then enable GC or a cron so it does not fill again.
You can also TRUNCATE TABLE modx_session; in SQL with the same effect.
Installer check¶
New installs run _checkSessionsGarbageCollector(). If probability is 0, Setup tries ini_set('session.gc_probability', 1) for the setup process and shows a warning with the current probability and divisor. A successful ini_set during Setup does not permanently fix the server. You still need php.ini, pool config, or a cron for production.
Related settings¶
| Setting | Role |
|---|---|
session_handler_class |
Default MODX\Revolution\modSessionHandler stores sessions in the DB. Empty uses PHP’s default (often files). |
session_gc_maxlifetime |
Age in seconds before GC deletes a session row. Default 604800. |
session_cookie_lifetime |
Browser cookie lifetime. Keep in mind next to GC max lifetime. |
session_cookie_secure, session_cookie_httponly, session_cookie_samesite, session_cookie_path, session_cookie_domain |
How the session cookie is set (HTTPS-only, no JS access, SameSite, scope). |
cache_db_session, cache_db_session_lifetime |
Cache session rows in the MODX cache instead of reading them from the table on every request; lifetime defaults to a quarter of session_gc_maxlifetime. |
session_enabled / anonymous_sessions |
When sessions start for front-end traffic. Note: session_enabled is not shipped as a system setting — core reads it with a default of true, so you have to create it manually (System Settings → Add) if you want to toggle it. |
See also¶
Support the team building MODX with a monthly donation.
The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.
Backers
Budget
$194 per month—let's make that $500!
Learn more










