Ottimizzare sessioni PHP su VPS gestite senza supporto tecnico: guida avanzata per Debian e Ubuntu
A giugno 2025 il responsabile IT di una PMI lombarda - azienda di servizi assicurativi con un portale Laravel 10 usato da circa 200 agenti commerciali distribuiti sul territorio - mi ha chiesto aiuto per un problema che si manifestava solo nelle ore di punta: tra le 9:00 e le 11:00 del mattino, quando tutti gli agenti si collegavano contemporaneamente, il portale diventava "impossibile da usare". Le pagine caricavano in 8-12 secondi, le chiamate AJAX per il calcolo dei preventivi restavano in attesa indefinitamente, e diversi agenti venivano disconnessi improvvisamente con errori CSRF token mismatch. Nel resto della giornata il portale funzionava normalmente. Il VPS era un Hetzner CPX31 - 8 vCPU, 16 GB RAM, 160 GB NVMe - con Debian 12, Nginx, PHP-FPM 8.2, MySQL 8.0 e Redis 7 (usato solo come cache, non per le sessioni).
La diagnosi ha rivelato che il collo di bottiglia non era CPU, RAM o MySQL - era il session locking di PHP su filesystem. Quando PHP usa il driver sessioni files (il default), ogni richiesta che apre una sessione acquisisce un lock esclusivo sul file di sessione. Se lo stesso utente ha due tab aperte o la pagina fa chiamate AJAX parallele, la seconda richiesta resta in attesa che la prima rilasci il lock. Con 200 utenti simultanei e un'applicazione che faceva 3-4 chiamate AJAX per pagina, i lock si accumulavano a cascata e il risultato era un degrado progressivo delle performance fino al blocco completo.
In questo articolo ti racconto come ho risolto il problema migrando le sessioni da file a Redis, e perché questa migrazione è più delicata di quanto sembri - il session locking in Redis richiede configurazione esplicita, e senza di essa rischi di scambiare un problema di performance con un problema di corruzione dati.
Stai cercando un Consulente Informatico esperto per ottimizzare le performance del tuo applicativo PHP? Nel mio profilo professionale trovi l'esperienza concreta su stack LEMP, Redis e ottimizzazione di applicazioni Laravel e Symfony. Contattami per una consulenza diretta.
Perché le sessioni PHP su file diventano un collo di bottiglia?
Il driver sessioni files di PHP salva ogni sessione come un file nella directory configurata in session.save_path (di default /var/lib/php/sessions su Debian). Quando session_start() viene chiamato, PHP apre il file della sessione e acquisisce un lock flock() esclusivo - nessun altro processo può leggere o scrivere quel file fino a quando la richiesta corrente non termina e rilascia il lock con session_write_close().
Questo meccanismo è sicuro per la consistenza dei dati - impedisce che due richieste simultanee scrivano sulla stessa sessione creando corruzione - ma è catastrofico per le performance quando un'applicazione fa richieste parallele per lo stesso utente. Laravel, con le sue chiamate AJAX per componenti Livewire, polling di notifiche, calcoli asincroni e prefetch di dati, genera regolarmente 3-5 richieste parallele per ogni azione dell'utente. Con il driver files, queste richieste vengono serializzate - la seconda aspetta la prima, la terza aspetta la seconda - trasformando 5 richieste parallele da 200ms ciascuna in una catena sequenziale da 1 secondo.
# Verificare il driver sessioni attuale
php -i | grep "session.save_handler"
# Output: session.save_handler => files
# Contare quante sessioni ci sono sul disco
ls /var/lib/php/sessions | wc -l
# Se il numero è nell'ordine delle decine di migliaia, hai un problema
# Verificare il garbage collection
php -i | grep "session.gc"
# gc_probability/gc_divisor determina la frequenza di puliziaUn secondo problema del driver files su VPS è la garbage collection. PHP pulisce le sessioni scadute in modo probabilistico: a ogni richiesta c'è una probabilità di gc_probability/gc_divisor (di default 1/100 su Ubuntu, 0/0 su Debian con cron separato) che venga attivata la pulizia. Se il garbage collection è disabilitato (come su Debian, dove un cron job separato in /etc/cron.d/php se ne occupa) e il cron non funziona, i file di sessione si accumulano fino a esaurire gli inode del filesystem - un problema che ho documentato nell'articolo sullo spazio disco.
La migrazione a Redis: la procedura operativa
Redis gestisce le sessioni in memoria, con latenza nell'ordine dei microsecondi anziché dei millisecondi del disco. Ma il vantaggio principale non è la velocità - è l'eliminazione del file locking. Con Redis, il lock della sessione è gestito a livello di estensione PHP (phpredis) con un meccanismo di spinlock che è ordini di grandezza più veloce del flock() su filesystem.
La migrazione ha tre passaggi: installazione, configurazione PHP e configurazione del locking.
# 1. Installare Redis e il modulo PHP (se non già presenti)
apt install -y redis-server php8.2-redis
systemctl enable redis-server
# 2. Hardening base di Redis (OBBLIGATORIO)
# In /etc/redis/redis.conf:
# bind 127.0.0.1 (solo localhost, MAI 0.0.0.0)
# requirepass "password_lunga_e_sicura"
# maxmemory 512mb
# maxmemory-policy allkeys-lru
systemctl restart redis-server
# 3. Verificare che Redis risponda
redis-cli -a "password_lunga_e_sicura" ping
# Output: PONGL'hardening di Redis è un passaggio non negoziabile - un Redis esposto senza password è un vettore di attacco critico che ho documentato nell'articolo sull'hardening di server Debian e Ubuntu. Nel caso del cliente lombardo Redis era già installato per la cache di Laravel, ma configurato senza password e con bind 0.0.0.0 - un altro problema che ho corretto durante l'intervento.
Configurazione PHP per sessioni Redis con locking
La configurazione in php.ini (o meglio, in un file dedicato per non toccare il php.ini principale):
# /etc/php/8.2/fpm/conf.d/30-sessions-redis.ini
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?auth=password_lunga_e_sicura"
# CRITICO: abilitare il session locking in Redis
# Senza questa configurazione, richieste parallele possono corrompere la sessione
redis.session.locking_enabled = 1
redis.session.lock_wait_time = 50000
redis.session.lock_retries = 2000
redis.session.lock_expire = 60Il blocco redis.session.locking_enabled = 1 è il passaggio che la maggior parte delle guide online omette, e la sua assenza causa problemi subdoli: senza locking, due richieste parallele possono leggere e scrivere la stessa sessione simultaneamente, producendo corruzione silente - flash message che scompaiono, dati del carrello che si sovrascrivono, token CSRF che diventano invalidi. L'estensione phpredis supporta il locking con uno spinlock configurabile: lock_wait_time (microsecondi tra un tentativo e l'altro) e lock_retries (numero massimo di tentativi) controllano quanto tempo una richiesta è disposta ad aspettare prima di rinunciare. Con i valori sopra, il timeout massimo è 2000 × 50000µs = 100 secondi - più che sufficiente per qualsiasi richiesta legittima.
# Applicare e verificare
systemctl restart php8.2-fpm
# Verificare che il driver sia cambiato
php -i | grep "session.save_handler"
# Output: session.save_handler => redis
# Verificare che le sessioni vengano create in Redis
redis-cli -a "password_lunga_e_sicura" keys "PHPREDIS_SESSION:*" | wc -lConfigurazione Laravel per sessioni Redis
Per applicazioni Laravel, la configurazione delle sessioni è nel file config/session.php e nel .env. La migrazione richiede un'attenzione particolare perché Laravel ha il suo driver sessioni che si sovrappone alla configurazione PHP:
# .env di Laravel
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_CONNECTION=session
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=password_lunga_e_sicura
REDIS_PORT=6379Il SESSION_CONNECTION=session punta a una connessione Redis dedicata in config/database.php - uso sempre un database Redis separato (es. database 1) per le sessioni, distinto da quello della cache (database 0), per poter fare FLUSHDB sulla cache senza distruggere le sessioni degli utenti loggati.
Quando le sessioni su file sono ancora la scelta giusta
Non ogni VPS ha bisogno di Redis per le sessioni. Se la tua applicazione ha meno di 30-50 utenti simultanei, gira su un singolo server, e non fa chiamate AJAX parallele intensive, il driver files su NVMe è perfettamente adeguato - e ha il vantaggio di non introdurre una dipendenza in più nello stack. Redis aggiunge complessità operativa: un servizio in più da monitorare, da aggiornare, da backuppare (se le sessioni devono sopravvivere ai riavvii), e da mettere in sicurezza.
La mia regola pratica è: usa files finché non hai un problema misurabile di performance legato alle sessioni. Se il tuo response time nelle ore di punta è accettabile e non hai logout casuali o errori CSRF, il driver files funziona e non c'è ragione di cambiarlo. Ma il momento in cui vedi i sintomi descritti sopra - richieste serializzate, AJAX in attesa, CSRF failure intermittenti - la migrazione a Redis è la soluzione più efficace e meno invasiva rispetto a riscrivere il codice applicativo.
Un'alternativa intermedia che uso su VPS con traffico moderato è aggiungere session_write_close() nelle rotte che non modificano la sessione - questo rilascia il lock del file appena possibile, riducendo la finestra di serializzazione. In Laravel si implementa con un middleware:
// Middleware che chiude la sessione in scrittura dopo la lettura
// Utile per rotte API che leggono la sessione ma non la scrivono
public function handle($request, Closure $next)
{
$response = $next($request);
session_write_close();
return $response;
}Questo approccio è un palliativo - non elimina il problema del locking come Redis, ma lo mitiga significativamente per le rotte read-only. Lo uso come primo intervento quando la migrazione a Redis non è immediatamente fattibile.
Monitoring delle sessioni Redis in produzione
Dopo la migrazione, il monitoring delle sessioni diventa più semplice perché Redis espone metriche native. Lo script che uso per verificare lo stato delle sessioni:
# Numero di sessioni attive in Redis
redis-cli -a "password" dbsize
# Output: (integer) 847 - 847 sessioni attive
# Memoria usata dalle sessioni
redis-cli -a "password" info memory | grep used_memory_human
# Output: used_memory_human:23.45M
# Verificare che il locking funzioni (no deadlock)
redis-cli -a "password" info clients | grep blocked_clients
# Se blocked_clients > 0 per periodi prolungati, c'è un problema di lockingIl parametro blocked_clients è il canary: se resta sopra zero per più di pochi secondi, significa che delle richieste sono bloccate in attesa del lock della sessione. In condizioni normali deve restare a zero o oscillare brevemente durante i picchi. Un valore persistentemente alto indica un lock non rilasciato - tipicamente un processo PHP che è crashato senza completare session_write_close(). La soluzione è il lock_expire che ho configurato a 60 secondi: dopo un minuto, il lock scade automaticamente e la sessione viene sbloccata.
Per un monitoring più completo dell'infrastruttura PHP - incluse le metriche di PHP-FPM, MySQL e Redis in un'unica dashboard - ho descritto l'approccio nell'articolo sull'ottimizzazione performance PHP su Hetzner e OVH.
Come si misura la sicurezza delle sessioni PHP?
La migrazione a Redis risolve il problema della performance, ma la sicurezza delle sessioni richiede configurazione esplicita indipendentemente dal driver. I parametri che verifico su ogni VPS:
# Parametri di sicurezza sessione (in php.ini o conf.d separato)
session.cookie_httponly = 1 # impedisce accesso JS al cookie
session.cookie_secure = 1 # cookie trasmesso solo via HTTPS
session.cookie_samesite = "Lax" # protezione CSRF base
session.use_strict_mode = 1 # rifiuta session ID non generati dal server
session.use_only_cookies = 1 # impedisce session ID nell'URL
session.sid_length = 48 # lunghezza session ID (default 32, meglio 48)
session.sid_bits_per_character = 6 # entropia per carattereIl session.use_strict_mode = 1 è particolarmente importante: senza di esso, un attaccante può forzare un session ID arbitrario (session fixation) e attendere che la vittima lo usi per autenticarsi. Con strict mode, PHP rifiuta session ID che non ha generato e ne crea uno nuovo - eliminando il vettore di attacco alla radice.
Il risultato sul portale assicurativo
Dopo la migrazione a Redis con locking configurato, i numeri sono cambiati radicalmente. Il response time delle chiamate AJAX è passato da 8-12 secondi a 80-120 millisecondi. I logout casuali sono scomparsi. Gli errori CSRF token mismatch sono scesi da decine al giorno a zero. Il tutto senza toccare una riga di codice applicativo - solo configurazione infrastrutturale.
Il costo dell'intervento è stato quattro ore di consulenza. Il costo del problema, nelle sei settimane in cui era rimasto irrisolto, era stato una perdita di produttività stimata dal titolare in 200 agenti × 30 minuti/giorno di tempo perso = 100 ore/giorno di produttività bruciata. Le sessioni PHP sono uno di quei componenti infrastrutturali che, quando funzionano, sono invisibili - e quando non funzionano, paralizzano l'intera operatività.
Se il tuo applicativo PHP diventa lento nelle ore di punta, se gli utenti si lamentano di logout improvvisi o di dati persi tra una pagina e l'altra, e se stai usando il driver sessioni files con più di cinquanta utenti simultanei, la migrazione a Redis è quasi certamente la soluzione. L'ho documentata decine di volte su VPS di PMI italiane, e il pattern è sempre lo stesso: performance che migliorano di un ordine di grandezza con un intervento di poche ore. Contattami e facciamo una diagnosi del tuo stack.