Making PHP Sessions Last Until Browser Closes

cookiesgarbage-collectionPHPsession

We have an issue where sessions are ending after a short time, 1440 seconds according to gc_maxlifetime settings which seems consistent with the issue. The PHPSESSID says it will expire when the session ends – I guess when the browser closes? I suppose before then though the GC is cleaning out expired sessions so the PHPSESSID, which outlives the session on the server, matches no session. Is that right? I guess a new PHPSESSID cookie is issued when the session is re-created?

Extending the sessiongc.maxlifetime is one option, we need to consider how the live site will handle the additional session data on the server. Moving sessions to Redis is an option here too.

Alternatively, in addition, is it possible to regenerate the session without losing session data? For example, each time the user opens a page we'll assume they are still active – so regenerate the session thus extending it's lifetime again to the 1440 seconds (or whatever we extend to). So, even if we have a short duration session the session is only destroyed when the user remains inactive (makes no more page loads) for that duration of time – so if I continue to click every 10 minutes even I'll never be signed out.

I'd also like to note we have millions of page views, and a few thousand authenticated users at any given time. So simple extending gc_maxlifetime is something we have to tread carefully with I think.

Best Answer

We run an AJAX call on our site which sends a heartbeat to the server once every couple minutes, keeping the session alive as long as the page is open on the browser. Once the page is closed, the session expires on its own.

Here's a snippet of the code I wrote to accomplish this. Please excuse all the cardiac terminology :)

/**
 * Heartbeat singleton
 */
var Heart = {
    url:         'https://my.domain.tld/EKG', // server script to hit
    logging:     false, // log to console for debugging
    pulse:       300, // heartbeat interval in seconds
    maxTimeouts: 3, // max timeouts before "heart attack" (stop)
    sessionName: 'PHPSESSID', // session cookie name

    // leave these alone
    timeouts:    0,
    timer:       null,
    sessionId:   null,

    /**
     * Begin heartbeats
     */
    start: function() {
        Heart.getSessionId();
        Heart.timer = setInterval(Heart.beat, Heart.pulse * 1000);
    },

    /**
     * Stop heartbeats
     */
    stop: function() {
        clearInterval(Heart.timer);
    },

    /**
     * Send single heartbeat
     */
    beat: function() {
        $.ajax({
            url:     Heart.url,
            headers: {
                'X-Heartbeat-Session': Heart.sessionId
            },
            success:  Heart.thump,
            timeout:  Heart.arrhythmia,
            error:    Heart.infarction
        });
    },

    /**
     * Successful heartbeat handler
     */
    thump: function() {
        Heart.log('thump thump');
        if (Heart.timeouts > 0)
            Heart.timeouts = 0;
    },

    /**
     * Heartbeat timeout handler
     */
    arrhythmia: function() {
        if (++Heart.timeouts >= Heart.maxTimeouts)
            Heart.infarction();
        else
            Heart.log('skipped beat')
                 .beat();
    },

    /**
     * Heartbeat failure handler
     */
    infarction: function() {
        Heart.log('CODE BLUE!! GET THE CRASH CART!!')
             .stop();
    },

    /**
     * Log to console if Heart.logging == true
     */
    log: function(msg) {
        if (Heart.logging)
            console.log(msg);

        return Heart;
    },

    /**
     * Parse cookie string and retrieve PHP session ID
     */
    getSessionId: function() {
        var name    = Heart.sessionName + '=',
            cookies = document.cookie.split(';');

        for (var i = 0; i < cookies.length; i++) {
            var cookie = cookies[i];

            while (cookie.charAt(0) == ' ')
                cookie = cookie.substr(1);

            if (cookie.indexOf(name) == 0) {
                Heart.sessionId = cookie.substr(name.length, cookie.length);
                break;
            }
        }
    }
};

// Start the heart!
Heart.start();

NOTE: This script requires jQuery >= 1.5 Also, if the script you're calling is on a different domain (even a different subdomain) then you need to either change the AJAX call to JSONP or add an Access-Control-Allow-Origin: * header to the response. See HTTP access control (CORS) for more info.