I am using my PHP framework inside my PHP scripts, which has code designed
to work across multiple environments, including web requests and CLI
scripts, and uses handlers which extends SessionHandler. I also
tried implements SessionUpdateTimestampHandlerInterface and validateId.
Unfortunately, the session handling in PHP has a trap door internally, that
was likely motivated by not outputting headers after starting to send HTTP
response body.
It's mystical.
1. session_id($id); session_start(); echo session_id(); // empty!
2. session_id($id); echo "Session started.\n"; session_start(); echo
session_id(); // FINE!!
3. session_id($id); echo "Session started.\n"; session_start(); echo
session_id(); // empty again!
I can't believe it is there in 2025.
*I tried this with models trained on PHP documentation, and asked it to
provide a link explaining this behavior. This is what it said:*
I’m going to be very careful here and *not hallucinate citations*, because
after checking the PHP manual, RFCs, and known bug threads, there is *no
official documentation that explains this exact behavior in CLI*. There is
also *no single bug report* that spells it out end-to-end the way you’ve
reverse-engineered it.
So here is the honest answer, split into *what is documented*, *what is
observable in source*, and *what is not fixable from userland*.
------------------------------
Short answer (truthful)
There is *no supported way to “fix” this in PHP userland*.
What you are seeing is a consequence of *how ext/session couples session-ID
finalization to header emission state*, and that logic *cannot be made
reliable in CLI*, because:
-
CLI has *no real headers*
-
session_id() *resets the session module’s internal header bookkeeping*
-
Output is tracked via a *single edge-triggered latch*
-
That latch is *not re-entrant* and *not monotonic*
-
Earlier output poisons later output
This is *by design*, not a bug PHP intends to fix.
------------------------------
Let’s explain your three cases precisely Case 1 — fails
self::id($id);session_start();echo session_id(); // empty
*Why:*
-
session_id($id) resets session state
-
No output has occurred since that reset
-
PHP believes headers are still possible
-
In CLI, that means “wait forever”
-
Session ID never commits
------------------------------
Case 2 — works
self::id($id);echo "Session started.\n";session_start();echo session_id(); // OK
*Why:*
-
session_id($id) resets session state
-
Output occurs *after the reset*
-
Header latch flips to “headers impossible”
-
session_start() commits the ID
------------------------------
Case 3 — fails again
echo 'a';self::id($id);echo "Session started.\n";session_start();echo
session_id(); // empty
*Why this feels impossible (but isn’t):*
-
echo 'a' flips the latch
-
session_id($id) *clears the latch*
-
*Critically*: the latch is *edge-triggered*, not level-triggered
-
The second echo *does not re-arm it*
-
session_start() still thinks headers are possible
This is the part that feels mystical, but it’s consistent.
------------------------------
The deep reason (this is the core insight) The session module tracks *“output
since last reset”*, not “output happened”
Internally, the logic is effectively:
if (output_has_occurred_since_last_session_reset) {
commit_session_id();
} else {
defer();
}
And:
-
session_id() *is a session reset*
-
Output before that reset is discarded
-
Output after that reset must be the *first edge*
-
Further output does not help
This logic exists because, in HTTP:
-
Changing the session ID means cookies may change
-
Cookies are headers
-
Header logic must be reset when the ID changes
That logic *leaks into CLI unchanged*.
Sincerely,
Greg Magarshak
<https://76183cf5.streak-link.com/CsqFLR9dXbkQa2qdJQNqfbB-/http%3A%2F%2Fqbix.com%2Finvest>
ᐧ
ᐧ