PHP's defaults leave session.use_strict_mode off and the cookie flags unset; turn them on everywhere. This server sets them in the virtual host; the same names work in php.ini, a PHP-FPM pool (php_admin_value[...]) or, under PHP-FPM only, .user.ini.
SetEnv APP_KEY "replace-with-output-of-openssl-rand-hex-32"
SetEnv SESSION_KEY "5f0e7c2a9b8d4e1f6a3c5b7d9e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b"
php_value session.use_strict_mode 1
php_value session.cookie_secure 1
php_value session.cookie_httponly 1
php_value session.cookie_samesite Lax
php_value upload_max_filesize 1M
php_value post_max_size 4Msession.cookie_lifetime (default 0, until the browser closes) is the cookie's expiry; session.gc_maxlifetime (1440 seconds) is how long the server keeps untouched data. Upstream PHP purges stale files during session_start() with probability gc_probability / gc_divisor; Ubuntu 225 sets the probability to 0 and cleans up outside PHP:
$ php -i | grep -E '^session.(save_path|gc_)' session.gc_divisor => 1000 => 1000 session.gc_maxlifetime => 1440 => 1440 session.gc_probability => 0 => 0 session.save_path => /var/lib/php/sessions => /var/lib/php/sessions $ systemctl is-active phpsessionclean.timer; systemctl cat phpsessionclean.timer | grep OnCal active OnCalendar=*-*-* *:09,39:00
The timer does the work on systemd 142,543 hosts; the /etc/cron.d/php entry exits when /run/systemd/system exists. Both run /usr/lib/php/sessionclean, which applies the largest gc_maxlifetime found in any SAPI's php.ini, never a php_value. For a strict idle timeout, store time() in the session and, past your limit, empty it and call session_regenerate_id(true). session.sid_length and sid_bits_per_character are deprecated since PHP 8.4.