Session database updates race condition/bad logic for keep-alives

Author: vr-jackCreated Sep 18, 2026Updated Sep 21, 2026

https://github.com/roundcube/roundcubemail/blob/4b54c2acfb54d5ee3d1c281ca7f143bed0dea804/program/lib/Roundcube/session/db.php#L188

Discovered this in the EPEL 1.5.15 version, but despite time working differently in the newer version, the decision to update is still the same.

I realize that the reason for this is to limit updates, but it creates problems in certain configurations.

For example, the default session_lifetime is 10 minutes. If you change when you check mail to 15 minutes, you'll get 2 keepalive tasks, a refresh task, and then another keepalive task. Because the numbers work out to halflife of the session_time, depending on when the request is made, we can exceed the session_lifetime and err=session.

Here is what it looks like in the database from a 2 minute session_lifetime using a 3 minute refresh (same scenario as 10m/15m).

| n79hqq0vr7mqmglctd4hpjktvf | 2026-09-18 15:27:01 | keep-alive | n79hqq0vr7mqmglctd4hpjktvf | 2026-09-18 15:28:08 | keep-alive | n79hqq0vr7mqmglctd4hpjktvf | 2026-09-18 15:28:08 | refresh (didn't update) | n79hqq0vr7mqmglctd4hpjktvf | 2026-09-18 15:30:12 | keep-alive session failure

I changed it to /3 for testing and watched the updates. Notice here that the refresh is 2 seconds too fast and wouldn't have updated with /2. The keep-alive in this case stayed within 2 minutes and wouldn't create an error, but this is a race condition.

| u787n83ij7mgvlb4k2qb85kcbv | 2026-09-18 16:23:33 | keep-alive | u787n83ij7mgvlb4k2qb85kcbv | 2026-09-18 16:24:34 | keep-alive | u787n83ij7mgvlb4k2qb85kcbv | 2026-09-18 16:25:32 | refresh | u787n83ij7mgvlb4k2qb85kcbv | 2026-09-18 16:26:33 | keep-alive

Even with keep-alives only (which will always be session_lifetime/2 if session_lifetime is <= 30 minutes, there is the possibility of time shifts causing sporadic session errors, but I believe why refresh is more consistent at being early is that refresh may use a different timer than keep-alive despite prohibiting the keep-alive and so it can fire earlier than expected more consistently and fail to update the session time.

At a minimum, there should be a buffer, so perhaps subtract 10-30 seconds? Ironically, the 1.5 code before the time was changed to expired uses / 3 for redis, memcached, and memcache, but apparently got changed to / 2 to match db when updated to the new time scheme.