Bugs de timezone: por que seu SaaS mostra a data errada?

O cliente salva 2 de outubro e vê dia 1. Um sistema fictício de projetos mostra como uma data ganha uma meia-noite indesejada e o que testar antes de corrigir.

Duas folhas de calendário deslocadas entre luz e sombra, com quadrados verde-lima marcando dias em posições diferentes.

O produto e o cliente desta história são hipotéticos. A conversão de data é um exemplo reproduzível em JavaScript, não o relato de um problema no Sinalio.

Um cliente abre um projeto no seu SaaS, escolhe 2 de outubro como data de entrega e salva. O formulário fecha. Na lista de projetos, aparece 1º de outubro.

Ele abre o formulário de novo. O dia 2 continua selecionado. Agora há duas respostas para uma pergunta simples: para quando ficou a entrega?

Quem investiga o chamado vê 2 de outubro em todas as telas. No banco, o valor é 2026-10-02. A API devolve a mesma coisa. O histórico não registra nenhuma alteração. Parece um problema de exibição, mas, para o cliente, já virou um problema do produto. Ele não sabe em qual tela confiar.

Antes de trocar o calendário ou revisar a tradução, vale acompanhar o valor pelo sistema. Em algum ponto do caminho, uma data pode ter ganhado um horário que ninguém escolheu.

A meia-noite que entrou no caminho

Imagine que a lista de projetos usa new Date(deliveryDate) antes de formatar a informação. A escolha parece natural: chegou uma data em texto, e o JavaScript tem um objeto chamado Date.

Para a string ISO que contém apenas a data deste exemplo, porém, o JavaScript assume meia-noite em UTC. A documentação da MDN descreve essa regra. O sistema transformou 2 de outubro no instante específico 2026-10-02T00:00:00.000Z.

Nesse instante, em São Paulo, ainda são 21h de 1º de outubro. Ao formatar o valor para esse fuso, o programa mostra corretamente o dia anterior. Este pequeno exemplo deixa a conversão visível:

const value = new Date("2026-10-02");

value.toISOString();
// "2026-10-02T00:00:00.000Z"

new Intl.DateTimeFormat("pt-BR", {
  timeZone: "America/Sao_Paulo"
}).format(value);
// "01/10/2026"

Mantenha o idioma em pt-BR e altere apenas o fuso para UTC: o resultado passa a ser 02/10/2026. Traduzir a interface não resolve essa conversão. Uma pessoa que usa o sistema em português pode estar em qualquer um desses fusos; o idioma, sozinho, não informa onde ela está.

O que o cliente escolheu de verdade?

A pergunta útil é qual compromisso aquele campo representa. No nosso sistema fictício de projetos, a data de entrega indica um dia do calendário. Todas as pessoas que abrem o projeto devem ver 2 de outubro, inclusive durante uma viagem.

Já o registro de quando o projeto foi enviado representa outra coisa. O envio aconteceu em um instante exato. Mostrar 21h para uma pessoa e meia-noite para outra pode estar correto.

A orientação do W3C sobre datas, horários e fusos descreve como valores flutuantes aqueles que precisam manter sua expressão no calendário, independentemente do fuso de quem os consulta. Transformar esse valor em um instante acrescenta uma informação capaz de mudar seu significado.

Essa distinção precisa fazer parte da decisão de produto antes de chegar ao formatador. “Data de entrega” pode indicar um dia inteiro. “Envios encerrados às 17h no fuso do workspace” define um limite de horário. São compromissos úteis, e a interface e o modelo de dados precisam preservar aquele que foi combinado.

Se a equipe acrescentar lembretes, também terá de decidir quando eles serão disparados. A data, sozinha, não responde a isso. O lembrete precisa de uma regra própria de horário e fuso, em vez de assumir silenciosamente a localização do servidor.

Acompanhe a data depois do formulário

O campo nativo de data do HTML já oferece uma separação útil. A apresentação segue a configuração regional do navegador, enquanto o valor usa yyyy-mm-dd, como explica a documentação da MDN. A pessoa escolhe o dia em um calendário familiar, e a aplicação recebe uma representação previsível.

O problema pode começar depois que esse campo cumpriu sua função. Um formatador compartilhado, a serialização da API ou o driver do banco pode converter o valor. Observe a representação real em cada passagem. No chamado fictício, o formulário e a API preservam 2 de outubro; é a lista que introduz a meia-noite.

Quando a tecnologia permitir, mantenha um campo que contém apenas uma data nessa forma durante o armazenamento e o transporte. Valide se ele representa um dia existente. Deixe claro que seu formatador recebe uma data sem horário, para que a próxima pessoa não aplique a ele as regras de um registro de evento.

No JavaScript, Temporal.PlainDate expressa diretamente esse conceito: uma data de calendário independente de fuso. Verifique os ambientes atendidos antes de adotá-lo. Na publicação deste artigo, a MDN ainda indica disponibilidade limitada. A decisão de modelagem continua válida se a sua aplicação usar outro tipo ou biblioteca para datas sem horário.

O conserto tentador pode esconder outro erro

Somar um dia faria a captura de tela de São Paulo parecer correta. Também alteraria uma data que já estava certa em outro lugar. Trocar a meia-noite inventada por meio-dia apenas desloca o problema entre os fusos do mundo.

Fixar UTC em um formatador pode corrigir aquele caminho de exibição quando ele lida, de propósito, com uma representação artificial em UTC. Trate isso como uma convenção documentada e de uso restrito. Aplicar a mesma regra a todos os registros de horário confundiria os horários dos eventos reais.

Também evite alterar os dados gravados antes de descobrir se o valor foi corrompido ou só exibido de forma incorreta. Nesta história, o dia 2 armazenado está certo. Uma migração de dados criaria um erro novo ao tentar eliminar o que aparece na tela.

Transforme o chamado em um teste de regressão

Reproduza o caminho do cliente em uma conta de testes: escolha a data, salve, abra a lista e volte ao formulário. Execute o cenário em UTC e America/Sao_Paulo. Inclua um fuso adiantado em relação a UTC, como Pacific/Kiritimati, para dificultar que um ajuste baseado em deslocamento passe despercebido.

Nesse campo sem horário, o dia do calendário deve sobreviver a todas as idas e voltas. Repita com o primeiro dia de um mês e de um ano, quando voltar um dia também muda outras partes da data. Depois, teste separadamente o horário de um evento real e confirme que sua exibição local muda como deveria.

O cliente que encontrou 1º de outubro não precisa de uma aula sobre UTC. Ele precisa que a entrega continue no dia escolhido. Quando formulário, lista e lembrete concordam sobre o significado daquela data, uma parte aparentemente simples do produto volta a ser confiável.

Escrito por

João Müller