Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
07 avril 2026

Ton SaaS rame “sans raison” ? J’ai déjà vu le coupable : des événements datés en 2226

Oui, un client peut t’envoyer des timestamps en 2226. Et oui, ça peut suffire à flinguer tes requêtes “last 30 days”, tes caches et ta vue Analytics.

11 min de lecture
14 vues
réactions
Partager :
Ton SaaS rame “sans raison” ? J’ai déjà vu le coupable : des événements datés en 2226

Si ton SaaS se met à ramer alors que rien n’a “vraiment” changé, pense à un truc très bête et très réel : tu as peut-être ingéré des événements datés dans le futur. Pas “5 minutes d’avance” à cause d’une horloge qui dérive. Je parle d’un délire du genre 2226. Et là, tout ton pipeline qui supposait gentiment que le temps avance dans le bon sens se met à faire n’importe quoi.

Je suis tombé sur un incident public où c’était littéralement la cause racine : des événements ~200 ans dans le futur ont suffi à rendre l’app lente, et à faire disparaître des événements de la vue d’analyse. Le bug paraît absurde. En prod, ça ne l’est pas.

Les symptômes typiques quand un timestamp futur s’invite dans ton produit

Le piège, c’est que ça ressemble à “de la perf” alors que c’est d’abord “de la qualité de données”. Côté front, tu vois une UI qui devient pâteuse, des pages Analytics qui chargent longtemps, ou des graphes qui se vident comme si tu n’avais plus de trafic. Côté back, tu commences à voir des requêtes qui explosent en temps de réponse, des timeouts, et des workers qui moulinent sur des agrégations censées être triviales.

Et surtout, tu as des bizarreries produit difficiles à expliquer : le “dernier événement” n’est plus le dernier, des paginations basées sur un tri temporel deviennent instables, des écrans “Last 7 days” n’affichent rien alors que tu sais qu’il y a des events, et des caches se mettent à durer beaucoup trop longtemps parce qu’un “max timestamp” part au plafond.

Pourquoi un événement en 2226 peut plomber tes requêtes (même avec des index)

Beaucoup de SaaS vivent sur un schéma mental simple : on stocke des événements, on les trie par date, on agrège sur des fenêtres “30 derniers jours”, et on affiche un “latest event” par user, par workspace, par intégration. Quand un timestamp futur arrive, il casse ce modèle à plusieurs endroits.

Le premier effet est brutal : le futur devient le nouveau “dernier”. Si tu as une requête du style “donne-moi le dernier event”, elle va désormais pointer sur cet événement daté en 2226. Tout ce qui dépend de “dernier état” peut se retrouver bloqué, figé, ou incohérent. Et c’est souvent plus grave que juste une ligne mal triée, parce que tu as probablement des chemins de code optimisés qui supposent que “le dernier” est proche de “maintenant” (caches, pré-agrégations, invalidations).

Deuxième effet, plus sournois : les fenêtres temporelles deviennent mensongères. Un écran “last 30 days” peut être implémenté avec une condition sur event_time > now() - interval. Ça continue de marcher… sauf que tes agrégations internes, elles, utilisent parfois des bornes calculées depuis le max timestamp vu. Et là tu te retrouves à recalculer des périodes absurdes, ou à générer des buckets vides, ou à décaler tes curseurs de pagination à des années-lumière.

Troisième effet : les traitements incrémentaux se sabotent. Beaucoup de jobs tournent en mode “je traite tout ce qui est > last_processed_time”. Si last_processed_time prend 2226, ton job vient de se tirer une balle : il ne traitera plus rien pendant… deux siècles. Résultat : des queues qui se stabilisent “bizarrement”, des dashboards qui ne bougent plus, puis des recalculs massifs quand tu corriges le problème à la main.

Event-time vs ingestion-time : la séparation qui t’évite ce genre de piège

Le garde-fou conceptuel le plus rentable, c’est d’arrêter de mélanger deux notions différentes : le moment où l’événement est censé être arrivé (event-time) et le moment où toi, plateforme, tu l’as reçu (ingestion-time).

En SaaS, l’event-time est souvent “fourni par le client”. Donc par définition, c’est une donnée non fiable. Parfois c’est juste un bug d’un SDK. Parfois c’est un problème d’unité (secondes vs millisecondes). Parfois c’est une timezone. Et parfois c’est juste quelqu’un qui a serialisé une date par défaut en 01/01/2226 parce que… pourquoi pas.

L’ingestion-time, elle, est une vérité opérationnelle. Tu peux t’en servir pour sécuriser la pagination, alimenter des index cohérents, gérer des TTL, et garder ton système “dans le présent” même si l’event-time est fantaisiste. Ensuite, au niveau produit, tu affiches l’event-time si tu y tiens, mais ton moteur ne se met pas à halluciner à cause d’une date client.

Validation à l’ingestion : bornes, skew max, unités, timezones

Le moment où tu peux encore empêcher le feu de se propager, c’est à l’entrée. Et non, “on fera confiance aux clients” n’est pas une stratégie. Une validation basique te protège déjà de 80% des catastrophes : rejeter ou mettre en quarantaine un événement dont le timestamp est trop loin dans le futur ou trop loin dans le passé par rapport à received_at (ou à l’horloge du serveur), avec un skew max assumé. Dans beaucoup de produits, +5 minutes ou +1 heure suffit. Si tu gères des devices offline, tu peux monter à 24h ou 7 jours, mais tu le fais consciemment.

La deuxième validation qui sauve des vies, c’est l’unité. Le classique : un client envoie des secondes, tu interprètes en millisecondes, ou l’inverse. Une date qui part en 51382 ou en 1970, c’est souvent ça. Plutôt que de faire de la magie silencieuse, je préfère détecter l’absurde et isoler l’événement. La magie “on corrige en douce” finit presque toujours par te coûter plus tard, parce que tu ne sais plus ce qui a été corrigé, quand, et comment.

// Exemple Node/TS : validation simple avant écriture
const MAX_FUTURE_SKEW_MS = 60 * 60 * 1000;        // +1h
const MAX_PAST_SKEW_MS   = 365 * 24 * 60 * 60 * 1000; // -365j (à adapter)

function parseEventTime(raw: unknown, receivedAt = new Date()): Date | null {
  if (typeof raw !== 'number' && typeof raw !== 'string') return null;
  const n = Number(raw);
  if (!Number.isFinite(n)) return null;

  // Détecte grossièrement secondes vs millisecondes
  const ms = n < 10_000_000_000 ? n * 1000 : n; // < ~2286-11-20 en secondes
  const dt = new Date(ms);
  if (Number.isNaN(dt.getTime())) return null;

  const skew = dt.getTime() - receivedAt.getTime();
  if (skew > MAX_FUTURE_SKEW_MS) return null; // futur suspect
  if (skew < -MAX_PAST_SKEW_MS) return null;  // trop vieux
  return dt;
}

// Si null : quarantaine (ou rejet) + alerte + métadonnées pour debug

Le point important, c’est ce que tu fais quand ça ne passe pas. Rejeter sec peut être OK si ton produit est strict et que tes clients peuvent corriger vite. Mais dans beaucoup de SaaS, la quarantaine est plus pragmatique : tu acceptes la requête, tu stockes l’événement ailleurs (table dédiée, bucket, file), tu n’alimentes pas tes agrégations, et tu te donnes le droit de corriger ou de reprocess plus tard. Tu évites l’incident, tu évites aussi de “casser” un client sur une erreur isolée.

Mettre en quarantaine sans te mentir : traçabilité, reprocess, backfill

La quarantaine n’est utile que si tu la rends actionnable. Concrètement, tu veux garder le payload brut, l’horodatage de réception, l’identifiant client, la version du SDK, et une raison claire (“timestamp_futur”, “timestamp_invalide”, “timezone_suspecte”). Sinon tu vas juste créer un cimetière de données que personne ne regarde.

Ensuite, il te faut un chemin de retour : soit un job qui tente une correction safe (par exemple, convertir secondes/millisecondes si le pattern est évident), soit une intervention manuelle outillée, soit un backfill après patch d’un SDK. Là aussi, expérience terrain : le backfill “à la main en prod” à 2h du matin, c’est rarement une bonne idée. Prévois un mode reprocess idempotent, qui ne duplique pas les événements et qui sait recalculer les agrégations affectées proprement.

Contraintes DB : protège-toi aussi côté stockage (PostgreSQL/MySQL)

Je ne suis pas fan du “tout dans l’app, la DB ne vérifie rien”. Sur une donnée aussi structurante qu’un timestamp d’événement, un garde-fou côté base peut t’éviter une ingestion toxique quand un code path bypass ta validation, ou quand tu as un worker legacy qui écrit encore directement.

Par contre, une contrainte du style “event_time < now() + interval '1 day'” est délicate en CHECK pur, parce que tu introduces une dépendance au temps courant et tu peux te créer des surprises. Le pattern que j’aime bien sur PostgreSQL, c’est un trigger qui rejette ou redirige vers une table de quarantaine. Ça reste explicite, loggable, et tu peux le faire évoluer sans migration destructive.

-- PostgreSQL : garde-fou simple côté DB (exemple rejet)
CREATE OR REPLACE FUNCTION reject_future_event_time()
RETURNS trigger AS $$
BEGIN
  IF NEW.event_time > (now() + interval '1 hour') THEN
    RAISE EXCEPTION 'event_time too far in the future: %', NEW.event_time
      USING ERRCODE = '22007';
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_reject_future_event_time
BEFORE INSERT OR UPDATE ON events
FOR EACH ROW
EXECUTE FUNCTION reject_future_event_time();

Et tant qu’on parle perf : si tu fais du time-series “à peu près”, pense à tes index. Beaucoup de requêtes critiques sont du type “par workspace, sur une fenêtre de temps, tri par date”. Un index composite sur (workspace_id, event_time desc) (ou (tenant_id, received_at desc) si tu pagines sur ingestion-time) peut te sauver. Mais si un seul event part en 2226, tu vas quand même polluer le haut de l’index. D’où l’intérêt de paginer/ordonner sur received_at quand tu veux de la stabilité opérationnelle.

Mitigation produit : éviter que l’Analytics devienne inutilisable

Un bon fix backend ne suffit pas toujours, parce que le temps de déployer, tu as déjà des données toxiques dans la base. Il te faut donc un mode dégradé côté UI qui évite de rendre l’écran inutilisable. Le genre de mitigation très simple qui marche bien : ne pas inclure par défaut des événements au-delà d’un horizon raisonnable dans les vues “Analysis”, ou offrir un fallback qui borne la recherche sur “> 100 jours” le temps de nettoyer. Ce n’est pas “pur”, mais c’est exactement ce qui empêche un incident de devenir une panne visible.

Si tu veux faire ça proprement sans mentir aux utilisateurs, affiche une bannière claire quand tu appliques ce filtre temporaire, et donne un export brut (ou un endpoint) pour ceux qui ont vraiment besoin de voir les données. Les gens acceptent le mode dégradé quand il est assumé. Ils détestent les écrans vides sans explication.

Tests et observabilité : détecter les dates impossibles avant que l’UI rame

Ce type d’incident passe souvent sous le radar parce que personne n’a de métrique “qualité des timestamps”. On surveille le p95, les 500, la charge DB. Mais on ne surveille pas “combien d’événements ont un event_time > now() + 1h”. Alors que c’est un signal parfait, bas bruit, et actionnable.

Au niveau tests, j’aime bien ajouter des cas qui font mal exprès sur le pipeline d’ingestion et sur les agrégations : un événement dans 10 ans, dans 200 ans, en 1970, et un timestamp en secondes interprété en millisecondes. L’objectif n’est pas de “tester JavaScript Date”. L’objectif est de prouver que ton système ne se met pas à recalculer des fenêtres absurdes, ne bloque pas un job incrémental, et ne fait pas exploser une requête “last 30 days”.

Mon avis : ce n’est pas un “edge case”, c’est une dette à payer tôt

Les timestamps fournis par l’extérieur sont une donnée hostile. Tu peux avoir les meilleurs index du monde, si tu laisses entrer une date en 2226, tu vas finir par la payer quelque part. La solution n’est pas un patch héroïque le jour de l’incident. C’est un contrat d’ingestion clair, une séparation event-time/ingestion-time, et une quarantaine qui te laisse respirer.

Et si tu ne veux retenir qu’un truc pratique : fais en sorte que “dernier événement” soit calculé sur une notion robuste (souvent received_at), et que event_time soit traité comme une information métier, pas comme un axe de pilotage opérationnel. Tu gagneras en perf, mais surtout en sérénité.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !