Fix PHP Session Expiring on Shared Hosting

Sessions expiring unexpectedly, users getting logged out randomly, or having to log in again every page load — these are classic PHP session problems on shared hosting. The root causes are different from VPS or dedicated servers because of how the shared environment handles session garbage collection and file permissions. This guide covers every fix, from simple .htaccess tweaks to database-backed sessions.

Why PHP Sessions Expire Fast on Shared Hosting

GC Triggered by Other Users

Shared hosting stores sessions in a common directory. When another user's script triggers GC (Garbage Collection), it may delete your sessions too.

session.gc_maxlifetime Too Short

Default is 1440 seconds (24 minutes). Without activity, the session expires and gets deleted on the next GC run.

Session Path Permissions

If PHP can't write to the session directory, sessions are never saved. On next request, PHP finds no session and treats the user as logged out.

Cookie Domain/Path Mismatch

If the session cookie is misconfigured, the browser won't send it back with requests — making every request appear as a new session.

Check Your Current Session Settings

Create a temporary session_info.php to see what the server is actually using (delete after checking):

<?php echo "gc_maxlifetime: " . ini_get('session.gc_maxlifetime') . "s\n"; echo "gc_probability: " . ini_get('session.gc_probability') . "\n"; echo "gc_divisor: " . ini_get('session.gc_divisor') . "\n"; echo "session.save_path: " . ini_get('session.save_path') . "\n"; echo "cookie_lifetime: " . ini_get('session.cookie_lifetime') . "\n"; echo "cookie_path: " . ini_get('session.cookie_path') . "\n";

Fix 1 — Set Values via .htaccess or php.ini

The simplest fix is to override session settings in .htaccess or a local php.ini:

# Add to .htaccess php_value session.gc_maxlifetime 86400 php_value session.cookie_lifetime 86400 php_value session.gc_probability 1 php_value session.gc_divisor 100
# Or add to php.ini (if DirectAdmin allows) session.gc_maxlifetime = 86400 session.cookie_lifetime = 86400 session.gc_probability = 1 session.gc_divisor = 100

86400 = 24 hours. Adjust to match your app's requirements. For most apps, 24 hours is a reasonable maximum.

Fix 2 — Move Sessions to a Private Directory

The core issue on shared hosting is that other users' GC runs can delete your sessions. Fix this permanently by storing sessions in a directory only your account can access:

<?php // Add BEFORE session_start() $session_path = __DIR__ . '/sessions'; if (!is_dir($session_path)) { mkdir($session_path, 0700, true); } ini_set('session.save_path', $session_path); ini_set('session.gc_maxlifetime', 86400); ini_set('session.cookie_lifetime', 86400); session_start();

Security: The sessions/ directory must not be web-accessible. Add a deny from all rule in sessions/.htaccess or place the folder outside public_html.

# Create sessions/.htaccess to block web access Deny from all

Fix 3 — Store Sessions in the Database (Recommended for Production)

The most robust solution — eliminates GC interference entirely by storing sessions in MySQL:

-- Create sessions table in MySQL CREATE TABLE php_sessions ( id VARCHAR(128) NOT NULL PRIMARY KEY, data TEXT NOT NULL, expires INT NOT NULL, INDEX idx_expires (expires) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php // Database Session Handler class DBSessionHandler implements SessionHandlerInterface { private PDO $db; private int $lifetime; public function __construct(PDO $db, int $lifetime = 86400) { $this->db = $db; $this->lifetime = $lifetime; } public function open($path, $name): bool { return true; } public function close(): bool { return true; } public function read($id): string { $stmt = $this->db->prepare( 'SELECT data FROM php_sessions WHERE id=? AND expires>?' ); $stmt->execute([$id, time()]); return $stmt->fetchColumn() ?: ''; } public function write($id, $data): bool { $expires = time() + $this->lifetime; $stmt = $this->db->prepare( 'REPLACE INTO php_sessions (id, data, expires) VALUES (?, ?, ?)' ); return $stmt->execute([$id, $data, $expires]); } public function destroy($id): bool { $stmt = $this->db->prepare('DELETE FROM php_sessions WHERE id=?'); return $stmt->execute([$id]); } public function gc($max_lifetime): int|false { $stmt = $this->db->prepare('DELETE FROM php_sessions WHERE expires<?'); $stmt->execute([time()]); return $stmt->rowCount(); } } // Initialize $pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8mb4', 'user', 'pass'); $handler = new DBSessionHandler($pdo, 86400); session_set_save_handler($handler, true); ini_set('session.gc_probability', 0); // Disable PHP GC — use expires column instead session_start();

Fix 4 — Secure Cookie Settings

If the session data exists but users still get logged out, the browser may not be sending the session cookie back. Fix cookie settings:

<?php // Set BEFORE session_start() ini_set('session.cookie_httponly', 1); // Prevent XSS access ini_set('session.cookie_secure', 1); // HTTPS only ini_set('session.cookie_samesite', 'Lax'); ini_set('session.cookie_domain', '.example.com'); // Include subdomains ini_set('session.use_strict_mode', 1); // Prevent session fixation session_start();

Fix 5 — Regenerate Session ID After Login

Always regenerate the session ID after a successful login to prevent session fixation attacks:

<?php // After successful login session_regenerate_id(true); // true = delete old session $_SESSION['user_id'] = $user['id']; $_SESSION['logged_at'] = time();

Debug with a Test Script

<?php session_start(); if (!isset($_SESSION['counter'])) { $_SESSION['counter'] = 0; $_SESSION['created_at'] = time(); } $_SESSION['counter']++; echo "Session ID: " . session_id() . "\n"; echo "Counter: " . $_SESSION['counter'] . "\n"; echo "Created: " . date('Y-m-d H:i:s', $_SESSION['created_at']) . "\n"; echo "Age: " . (time() - $_SESSION['created_at']) . "s\n";

Refresh the page repeatedly. If the counter keeps increasing, sessions are working. If the counter resets to 1 every time, the session is still broken.

Summary: Small app → move sessions to a private directory (Fix 2). Production app with many users → store sessions in MySQL (Fix 3). Always combine with secure cookie settings (Fix 4).

Comparing Session Storage Methods

Each session storage approach has different trade-offs. Choosing the right one for your application size and requirements leads to a permanent fix rather than a temporary workaround.

Storage Method Best For Advantages Limitations
Filesystem (Default)Local / developmentZero configuration neededOther users' GC can delete your sessions
Private directoryShared hosting, small appsEliminates cross-user GC, easy to implementStill relies on disk; disk-full events cause issues
MySQL databaseProduction apps with many usersVery stable, precise expiry controlAdds DB load; expired rows need cleanup
Redis / MemcachedVPS / high-traffic appsExtremely fast, supports multiple serversRequires extra PHP extensions; not on all hosts

Session Security — What to Harden When You Extend Session Lifetime

Longer-lived sessions are more convenient but carry greater security risk. Every fix that extends session lifetime should be paired with the following security measures.

Prevent Session Hijacking

Session hijacking occurs when an attacker steals a user's session ID and impersonates them. Key defences:

Prevent Session Fixation

Session fixation is when an attacker forces a victim to use a known session ID. One line of code prevents it:

<?php // Call immediately after successful login, before writing to $_SESSION session_regenerate_id(true); $_SESSION['user_id'] = $authenticated_user_id; $_SESSION['login_time'] = time(); $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];

Implement Both Idle and Absolute Timeouts

Relying solely on gc_maxlifetime is not enough for production applications. Enforce two separate expiry rules in PHP itself:

<?php session_start(); $max_idle = 1800; // Log out after 30 minutes of inactivity $max_absolute = 86400; // Force re-login after 24 hours regardless if (isset($_SESSION['last_active'])) { if ((time() - $_SESSION['last_active']) > $max_idle) { session_destroy(); header('Location: /login.php?reason=idle'); exit; } } if (isset($_SESSION['login_time'])) { if ((time() - $_SESSION['login_time']) > $max_absolute) { session_destroy(); header('Location: /login.php?reason=expired'); exit; } } $_SESSION['last_active'] = time();

Common Questions About PHP Sessions on Hosting

I increased gc_maxlifetime but sessions still expire early — why?

On shared hosting, another account may run GC with a shorter gc_maxlifetime setting and delete your session files in the process. This is the most common reason why simply increasing gc_maxlifetime does not fully solve the problem. Moving to a private session directory (Fix 2) or a database (Fix 3) eliminates this interference entirely because other users cannot touch your storage.

Does WordPress have the same PHP session problem?

WordPress does not use PHP's native $_SESSION at all — it uses its own Authentication Cookies (wordpress_logged_in_*, wordpress_*). WordPress login timeouts are caused by incorrect cookie domain settings, HTTPS/HTTP mixed content, or regenerated Secret Keys in wp-config.php. The fix is to set COOKIEPATH and COOKIE_DOMAIN correctly in wp-config.php and ensure the site runs on HTTPS consistently.

Can I use Redis for sessions on shared hosting?

Only if your hosting plan includes the phpredis or predis PHP extension. You can check with phpinfo() or ask support. For applications that need Redis, a VPS gives you a dedicated Redis service with full configuration control, starting at ฿500/month on AsiaGB.

PHP Hosting with Full Session Control

AsiaGB Hosting supports custom PHP session handlers, MySQL sessions, and private session paths. Starting at ฿500/year.

View Hosting Plans

View all cheap Thailand web hosting plans →