Partiamo dalla conclusione: un timestamp Unix non porta con sé alcun fuso orario. È solo il numero di secondi trascorsi dalle 1970-01-01 00:00:00 UTC. Le «8 ore di scarto» che vedi nascono quasi sempre nel momento in cui il numero diventa una stringa leggibile.
Che cos'è davvero un timestamp
Un timestamp Unix è un numero intero che rappresenta un istante. Non conosce «ora di Roma» né «ora di New York»: lo stesso istante produce lo stesso numero ovunque. Per questo memorizzare timestamp è la scelta più sicura: spostare il server o cambiare il fuso di sistema non li altera.
L'unica cosa da sorvegliare è la precisione: 10 cifre sono secondi, 13 sono millisecondi. Leggere un valore in millisecondi come secondi porta all'anno 50000; il contrario riporta a gennaio 1970. È un errore che compare in ogni linguaggio.
Tre origini dello scarto di 8 ore
1. Archivio e visualizzazione usano convenzioni diverse. Il back end salva in UTC, il front end formatta in ora locale: entrambi sono corretti, nessuno ha detto quale fuso intendeva.
2. Il server non è in UTC. Alcune immagini usano Asia/Shanghai per impostazione predefinita, altre UTC. Lo stesso codice scrive righe di log distanti 8 ore su due macchine. Se gli orari non tornano, controlla prima date e timedatectl.
3. Durante il parsing viene aggiunto un fuso implicito. Un valore come 2026-10-19 00:00:00 senza offset viene letto come ora locale da browser e Node, ma come UTC da alcune librerie. Una stringa, due letture, 8 ore di differenza.
Autoverifica rapida
Nel dubbio fai due cose: leggi lo stesso timestamp come UTC e come ora locale — se i risultati differiscono di un numero intero di ore è un problema di fuso; poi converti l'orario che ti aspettavi in timestamp e confrontalo con il valore nel log. Una differenza di 28.800 secondi è esattamente 8 ore.
Tre abitudini che eliminano gran parte dei problemi
- Salvare sempre timestamp UTC oppure ISO 8601 con offset (es.
2026-10-19T02:00:00Z) - Indicare esplicitamente il fuso in visualizzazione, senza affidarsi al default dell'ambiente
- Nelle API passare sempre un offset o dichiarare chiaramente che il valore è in UTC
In una frase
Salva in UTC, mostra nel fuso dell'utente, interpreta con un offset esplicito. Queste tre regole evitano la maggior parte delle segnalazioni «l'orario è sbagliato».
Un solo istante, cinque scritture
Scrivete lo stesso momento — 2026-10-19 02:00:00 UTC — in cinque modi diversi e l'origine della confusione diventa evidente:
1792375200— secondi Unix. Assoluto, senza ambiguità.1792375200000— millisecondi Unix. Assoluto, senza ambiguità.2026-10-19T02:00:00Z— ISO 8601, laZfinale indica UTC. Assoluto.2026-10-19T10:00:00+08:00— ISO 8601 con offset esplicito. Assoluto.2026-10-19 10:00:00— senza alcuna zona. Non è un istante: vale la zona locale del programma che lo interpreta.
I primi quattro indicano lo stesso punto sull'asse del tempo. Il quinto ne indica uno diverso su ogni macchina. Quasi ogni discussione sul fatto che «gli orari non tornano» nasce dal trattare il quinto come se fosse uno dei primi quattro.
I valori predefiniti cambiano da tecnologia a tecnologia
È qui che si rompono le cose. Lo stesso intento — «salvare un orario» — viene realizzato diversamente in ogni strato:
- MySQL:
TIMESTAMPconverte il valore in UTC usando la zona della sessione in scrittura e lo riconverte in lettura.DATETIMEmemorizza esattamente ciò che riceve. Tipo di colonna sbagliato e un client in un'altra zona legge un valore diverso. - PostgreSQL:
timestamptzmemorizza UTC conservando la semantica della zona;timestampnon memorizza alcuna zona. La documentazione consiglia il primo come predefinito. - Java:
Instantè un punto assoluto,LocalDateTimenon porta zona. Serializzate unLocalDateTimedirettamente in stringa e l'informazione sulla zona è persa. - Python:
datetimeesiste naive (senza zona) e aware (con zona), e sottrarre l'uno dall'altro solleva un'eccezione. Allegare la zona conzoneinfoè lo standard attuale. - JavaScript: un
Dateè sempre un timestamp in millisecondi, magetHours()etoString()restituiscono l'ora nella zona dell'ambiente, mentretoISOString()restituisce sempre UTC.
Una regola li copre tutti: qualsiasi tipo chiamato «ora locale» non porta informazioni di zona, e si deforma appena attraversa il confine di una macchina.
Niente abbreviazioni: usate i nomi IANA
La sola sigla CST può significare China Standard Time (UTC+8), Central Standard Time statunitense (UTC−6) o Cuba Standard Time (UTC−5). Nemmeno EST è sicuro: ignora l'ora legale, mentre la costa est degli Stati Uniti d'estate usa EDT (UTC−4).
Usate gli identificatori IANA: Asia/Shanghai, America/New_York, Europe/Berlin. Contengono la storia completa dell'ora legale, sono mantenuti nel database tz di sistema e restano corretti per qualsiasi anno.
Un dettaglio facile da dimenticare: la Cina ha osservato l'ora legale dal 1986 al 1991, quindi gli istanti estivi di quegli anni stanno a UTC+9. Asia/Shanghai lo gestisce correttamente, un UTC+8 scritto a mano sbaglia di un'ora.
Tre domande prima di leggere il codice
- È un istante o una lettura dell'orologio da parete? Se è un numero, il difetto è per forza nella formattazione. Se è una stringa, stabilite prima in quale zona è stata scritta.
- Chi produce, chi memorizza e il browser concordano sulla zona? Se no, da qualche parte esiste una conversione: è quella il sospetto.
- La differenza è un numero intero di ore? Otto ore, o 5,5 ore (India, UTC+5:30), sono un problema di zona. Pochi secondi di scarto sono più probabilmente sincronizzazione o precisione.
Queste tre domande eliminano metà delle possibilità prima ancora di aprire un file di log.