Como lidar com timezone em aplicações web
Lidar com timezone em aplicações web é um dos desafios mais persistentes no desenvolvimento de software moderno. Um erro aparentemente simples — como armazenar uma data sem considerar o fuso horário do usuário — pode gerar inconsistências graves, desde notificações enviadas no horário errado até registros financeiros incorretos. Este artigo aborda os fundamentos, estratégias práticas e armadilhas comuns para gerenciar timezones de forma robusta.
1. Fundamentos sobre timezone no ecossistema web
O primeiro princípio é claro: armazene tudo em UTC. UTC (Coordinated Universal Time) é o padrão de tempo independente de fusos horários e horário de verão. Ao salvar timestamps em UTC, você mantém um ponto de referência único e evita ambiguidades.
É crucial entender a diferença entre:
- Timezone: região geográfica com regras de horário (ex: America/Sao_Paulo)
- Offset: diferença em horas/minutos em relação ao UTC (ex: -03:00)
- Horário de verão (DST): ajuste sazonal que altera o offset temporariamente
O IANA Time Zone Database (tz database) é a base de dados oficial que mantém essas regras atualizadas. Sistemas modernos confiam nela para resolver conversões corretamente.
// Exemplo: diferença entre timestamp e timezone
// Timestamp UTC: 2025-01-15T14:00:00Z
// Timezone America/Sao_Paulo: 2025-01-15T11:00:00-03:00 (horário padrão)
// Timezone America/Sao_Paulo com DST: 2025-01-15T10:00:00-02:00 (se aplicável)
2. Coletando o timezone do usuário no frontend
No navegador, a API Intl oferece a maneira mais confiável de detectar o timezone do usuário:
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(userTimezone); // "America/Sao_Paulo", "Asia/Tokyo", etc.
Para maior precisão, combine com geolocalização:
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
// Enviar coordenadas para o backend que retorna timezone via API
fetch(`/api/timezone?lat=${position.coords.latitude}&lng=${position.coords.longitude}`)
.then(res => res.json())
.then(data => console.log(data.timezone));
},
() => console.log("Usando fallback:", userTimezone)
);
}
Fallbacks importantes: para usuários com JavaScript desabilitado, use cabeçalhos HTTP como Accept-Language ou permita configuração manual no perfil.
3. Armazenamento e manipulação de datas no backend
No servidor, sempre armazene timestamps como UTC:
// Armazenamento em ISO 8601 (recomendado)
"2025-01-15T14:00:00.000Z"
// Ou como Unix timestamp (segundos desde epoch)
1736949600
Cuidados com bancos de dados:
- TIMESTAMP WITH TIME ZONE (PostgreSQL): armazena em UTC internamente, mas aceita conversão
- TIMESTAMP WITHOUT TIME ZONE: perigoso — não guarda informação de fuso, assumindo timezone da sessão
Bibliotecas recomendadas:
Node.js/JavaScript (date-fns-tz):
const { format, utcToZonedTime } = require('date-fns-tz');
const utcDate = new Date('2025-01-15T14:00:00Z');
const timezone = 'America/Sao_Paulo';
const zonedDate = utcToZonedTime(utcDate, timezone);
const result = format(zonedDate, 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: timezone });
// "2025-01-15 11:00:00-03:00"
Luxon:
const { DateTime } = require('luxon');
const dt = DateTime.fromISO('2025-01-15T14:00:00Z', { zone: 'utc' });
const local = dt.setZone('America/Sao_Paulo');
console.log(local.toFormat('yyyy-MM-dd HH:mm')); // "2025-01-15 11:00"
4. Exibição de datas no fuso do usuário
No frontend, converta UTC para o timezone local:
const utcDate = new Date('2025-01-15T14:00:00Z');
const options = {
timeZone: 'America/Sao_Paulo',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
const formatter = new Intl.DateTimeFormat('pt-BR', options);
console.log(formatter.format(utcDate)); // "15 de janeiro de 2025 11:00"
Estratégia de renderização: para evitar discrepâncias entre server-side e client-side (hydration), envie timestamps UTC brutos e formate apenas no cliente. Se precisar renderizar no servidor, use o timezone do cabeçalho Accept-Language ou um cookie configurado.
5. Lidando com horário de verão (DST)
DST introduz dois problemas clássicos:
- Horário inexistente: quando o relógio adianta, 1h da manhã "pula" para 2h
- Horário ambíguo: quando o relógio atrasa, 1h da manhã ocorre duas vezes
Bibliotecas como Luxon tratam isso automaticamente:
const { DateTime } = require('luxon');
// Horário ambíguo (DST termina)
const ambiguous = DateTime.fromISO('2025-02-16T01:30:00', {
zone: 'America/Sao_Paulo'
});
console.log(ambiguous.offset); // -02:00 ou -03:00? Luxon escolhe o primeiro
// Para resolver explicitamente:
const first = DateTime.fromISO('2025-02-16T01:30:00', {
zone: 'America/Sao_Paulo',
setZone: true
}).set({ offset: -2 }); // Força offset do DST
6. Agendamentos e recorrências sensíveis a timezone
Para eventos recorrentes, nunca armazene apenas o offset — use o timezone IANA completo:
// Correto
{
"event": "Reunião semanal",
"startTime": "10:00",
"timezone": "America/Sao_Paulo",
"rrule": "FREQ=WEEKLY;BYDAY=MO"
}
// Errado (offset muda com DST)
{
"event": "Reunião semanal",
"startTime": "10:00",
"offset": "-03:00",
"rrule": "FREQ=WEEKLY;BYDAY=MO"
}
Para notificações, converta sempre para UTC do momento do agendamento:
// Agendamento para 10/03/2025 às 10:00 em São Paulo
const eventDate = DateTime.fromISO('2025-03-10T10:00:00', {
zone: 'America/Sao_Paulo'
});
const utcTimestamp = eventDate.toUTC().toISO();
// Enviar notificação quando UTC atingir esse timestamp
7. Testes e validação de timezone
Crie testes que simulem diferentes fusos e cenários de DST:
// Exemplo com Jest
describe('Timezone conversion', () => {
test('should convert UTC to America/Sao_Paulo correctly', () => {
const utcDate = new Date('2025-01-15T14:00:00Z');
const result = convertToTimezone(utcDate, 'America/Sao_Paulo');
expect(result.getHours()).toBe(11); // -3h no horário padrão
});
test('should handle DST transition correctly', () => {
// 16/02/2025 fim do DST em SP (relógio atrasa)
const utcDate = new Date('2025-02-16T03:00:00Z');
const result = convertToTimezone(utcDate, 'America/Sao_Paulo');
expect(result.getHours()).toBe(0); // Volta para 00:00
});
});
Use ferramentas como timezone-mock para simular fusos em ambiente de teste:
const timezoneMock = require('timezone-mock');
timezoneMock.register('America/Sao_Paulo');
// Todos os Date() agora usam esse fuso
8. Boas práticas e armadilhas comuns
Evite:
- Converter strings manualmente com new Date('2025-01-15') — isso assume timezone local
- Usar Date.parse() sem especificar timezone
- Confiar em APIs que retornam datas sem indicar o fuso (ex: "2025-01-15T14:00:00" sem "Z" ou offset)
Checklist final:
1. Armazenamento: sempre UTC, em formato ISO 8601 com "Z" ou Unix timestamp
2. Exibição: converta no frontend usando Intl.DateTimeFormat com timezone do usuário
3. Agendamento: armazene timezone IANA completo, não apenas offset
4. Logging: registre timestamps em UTC + timezone de origem para auditoria
// Log seguro
console.log(`Evento criado em ${new Date().toISOString()} (UTC) pelo usuário no fuso ${userTimezone}`);
Referências
- MDN Web Docs: Intl.DateTimeFormat — Documentação oficial sobre formatação de datas com timezone no navegador
- Luxon Documentation: Timezone Handling — Guia completo da biblioteca Luxon para manipulação de fusos horários
- date-fns-tz: Timezone Utilities — Biblioteca leve para conversão de timezones baseada em date-fns
- PostgreSQL: TIMESTAMP vs TIMESTAMPTZ — Documentação oficial sobre tipos de timestamp no banco de dados
- IANA Time Zone Database — Base de dados oficial de fusos horários mantida pela Internet Assigned Numbers Authority
- timezone-mock: Simulating Timezones in Tests — Ferramenta para mockar timezones em testes unitários JavaScript
- Storing UTC vs Timezone in Databases — Discussão técnica sobre boas práticas de armazenamento de datas no Stack Overflow