AnnonceNouveautés31 lectures
Ce qui reste ne suffit pas à décider : il faut savoir qui l’a pris
LeTock.fréditeur 10 pts ·
Publié le 24 septembre 2026.
Notre surveillance avait l’air de marcher et ne regardait rien.
Deux défauts dans le câblage posé le jour même, trouvés en allant lire les battements réellement enregistrés plutôt qu’en croyant le code. Le compte partait dans un corps JSON ; la route /api/ping le lit dans les paramètres d’URL, et rien d’autre. Les battements arrivaient donc avec traite vide — et le contrôle de volume « zéro », armé sur cette tâche précisément pour repérer un passage qui ne fait rien, n’avait rien à contrôler. Une surveillance qui a l’air de marcher est pire qu’une surveillance absente : elle occupe la place de celle qui aurait regardé. Et un battement de début partait vers /api/ping/<clé>/start, qui n’existe pas : un battement de mode « monitor » est un signal, pas une exécution ; il n’a ni début ni durée. Conséquence en cascade, celle qui coûtait le plus : la veille qui vérifie que la prospection ne ralentit pas les tâches des clients calculait sa fenêtre comme [début, début + durée], avec une durée toujours nulle. Fenêtre de longueur zéro, aucune exécution de client ne pouvait y tomber, verdict « indéterminé » pour toujours. Le passage envoie désormais sa durée en métadonnée, et la fenêtre est celle qui précède le battement. Un battement sans durée est écarté plutôt que de se voir inventer une fenêtre : mieux vaut moins d’observations qu’un échantillon rempli au jugé.
Le palier du 1er octobre autorise 1 500 messages froids ; la collecte n’en rendait que 134.
La question qui a fait trouver ça : « le 1er octobre, combien de messages ? » La chauffe répond 1 500 — jour 9, palier 6 000, dont le quart pour le froid. La collecte, elle, répondait 134, et c’est elle qui aurait gagné. Le goulot n’était plus le courrier, c’était nous. La cause était bête : la collecte visitait les domaines un par un, alors que rien ne l’y oblige — ce sont des sites différents, et presque tout le temps passé est de l’attente réseau. Ce n’est pas une question de politesse : chaque serveur d’en face voit toujours les mêmes cinq requêtes, à la même cadence ; ce qui change, c’est qu’on n’attend plus la réponse d’Angers pour frapper à Bordeaux. À douze de front, un passage visite sept mille domaines en quarante minutes et la journée rend environ 1 170 prospects. Le détail qui compte : des ouvriers qui puisent dans la même file, pas des paquets de taille fixe — un paquet contenant trois domaines muets finirait bien avant les autres, et la fin du lot se ferait à un seul ouvrier. La réserve suit : 42 000 visites par jour demandent deux jours d’avance en domaines qualifiés, donc un tirage nocturne de 80 000 candidats au lieu de 30 000. Et un test a cassé en chemin, pour une bonne raison : il écrivait « curseur + lot » sur une liste de 10 000, ce qui mesurait la fin de la liste dès que le lot a dépassé 5 800. Il est désormais dimensionné sur le lot.
Ta semaine, heure par heure — et où déplacer ce qui s’entasse à minuit.
Presque tout le monde écrit 0 0 * * *. C’est le premier cron qu’on apprend, et c’est celui qu’on recopie. Douze tâches partent alors à minuit pile, se disputent la même base et le même réseau, et trois dépassent leur délai — non pas parce qu’elles sont lentes, mais parce qu’elles sont ensemble. Chacune, prise seule, paraît raisonnable : ça ne se voit que sur une semaine, à plat. Le calendrier montre désormais, sous le mois, les sept derniers jours mesurés — pas prévus — heure par heure, dans ton fuseau. Et quand une case porte plus du double de la moyenne, l’écran dit laquelle, de combien, et quelles heures du même jour sont les plus calmes. Trois conseils au plus : trois qu’on applique valent mieux que vingt qu’on survole. Deux choses qu’on ne conseille jamais. La nuit profonde, 1 h – 4 h : y envoyer une tâche la rend inobservable, et le produit qui conseillerait ça est le même qui vend la détection des pannes silencieuses. Et rien du tout en dessous de cinquante exécutions sur la semaine — une recommandation tirée de trois points est une opinion déguisée en mesure. Quand il n’y a pas de conseil, l’écran dit pourquoi : une absence sans cause est indistinguable d’une panne de l’écran. Le regroupement se fait dans ton fuseau, pas en UTC puis décalé : un histogramme décalé d’une heure ressemble toujours à un histogramme, et il serait faux deux semaines par an.
Six passages prenaient 82 % du palier là où la règle en autorise 25.
Le calcul tenait en une ligne : « un quart de ce qui reste du palier ». C’était juste tant que la prospection tournait une fois par jour. Le jour où on l’a fait tourner six fois, chaque passage a pris un quart du RESTE — et six quarts successifs font 1 − 0,75⁶ = 82 % du palier. Rien ne s’en plaignait : chaque passage, pris seul, était parfaitement conforme. C’est la somme qui ne l’était pas, et personne ne regardait la somme. Ce plafond n’est pas un plafond de volume, il protège le mélange : un message attendu est ouvert, un message froid est signalé, et remplir tout le creux laissé par les clients avec du courrier froid tiendrait dans le palier tout en abîmant la réputation d’une adresse que tous les domaines partagent. Le calcul compte désormais ce que la prospection a déjà envoyé aujourd’hui, ce qui rend six passages équivalents à un seul. Effet de bord corrigé dans la foulée : la sortie anticipée « la part est prise » annonçait « zéro traité » à notre propre contrôle de volume alors qu’elle venait de mesurer vingt-cinq domaines — cinq fausses alertes par jour, dans le cas normal. Une alerte qui se trompe apprend à ne plus regarder le rouge.
Le constat est re-mesuré à l’instant de l’envoi : celui qui a corrigé entre-temps ne reçoit rien.
Le message entier tient sur une phrase : « en mesurant ton domaine, j’ai relevé ceci ». La démonstration du produit, la raison pour laquelle ce n’est pas un publipostage, la légitimité même de l’envoi — tout repose sur le fait que cette phrase soit vraie au moment où elle est lue. Or elle était mesurée d’un côté et envoyée de l’autre, et entre les deux il peut s’écouler des jours pendant lesquels quelqu’un pose très bien son DMARC. Lui écrire alors n’est pas une imprécision : c’est la seule erreur qui détruise exactement ce que le message essaie d’établir — celui qui la reçoit n’en conclut pas qu’on s’est trompé, il conclut que la mesure est inventée, et il a raison de le penser. Le stock écartait déjà les constats de plus de sept jours ; sept jours, c’est précisément le temps qu’il faut pour corriger ce qu’on vous a signalé ailleurs. Chaque domaine est donc re-mesuré juste avant d’écrire. S’il n’y a plus rien à dire, il sort de la file — et on ne le remesure pas en espérant qu’il se dégrade. Si le constat a changé de nature, c’est le frais qui part : il reste quelque chose de vrai à dire, et envoyer l’ancien serait mentir quand ne rien envoyer serait renoncer.
Il n’y a pas d’heures creuses : on est allé regarder avant de le supposer.
L’idée était de faire tourner les gros travaux « aux heures creuses ». Mesure sur sept jours de production : le serveur exécute ~410 tâches par heure, à toute heure, nuit comprise — ce sont nos propres sondes, qui tournent à intervalle fixe. Charge moyenne : 0,01. Caler un travail sur un creux mesuré à zéro aurait été inventer un fait, et le pire des faits inventés : celui qui a l’air prudent. La prospection tourne donc en priorité basse — Nice=10, CPUWeight=20, entrées-sorties en classe « idle », et Nice=15 pour les trente mille résolutions DNS du regarnissage nocturne. C’est vrai à toute heure, et ça le restera quand les clients arriveront, ce qu’aucun horaire figé ne peut promettre. Et on vérifie que ça reste vrai : une priorité est une intention déclarée à l’ordonnanceur, elle ne dit rien d’un verrou tenu trop longtemps ni d’un disque qui plafonne. Une veille compare donc la durée des exécutions des clients pendant nos passages et en dehors — leur latence, pas la nôtre, parce que la prospection a le droit d’être lente et que c’est précisément pour ça qu’elle est en priorité basse. Les fenêtres viennent des battements de notre propre tâche surveillée, pas d’une approximation par plage horaire. En dessous de vingt observations de chaque côté, le verdict est « indéterminé » — jamais « sain » : « je n’ai pas pu savoir » n’est pas « tout va bien ».
Notre propre prospection est surveillée par notre propre Cron — six battements par jour, contrôle de volume compris.
Un minuteur qui meurt ne dit rien. Un script qui échoue au démarrage ne dit rien. Une liste qui s’épuise ne dit rien. La campagne aurait pu s’arrêter pendant une semaine sans que personne ne s’en aperçoive, et nous serions très mal placés pour laisser ça chez nous. Il y a donc maintenant une tâche monitor dans LeTock, posée par le même chemin que l’écran et que l’API — createJob, avec une clé d’idempotence — qui attend six battements par jour (8 h 40, 10 h 40, … 18 h 40). Deux réglages qui comptent : la grâce est de 90 minutes, donc sous l’intervalle de deux heures — au-delà, un battement manquant serait couvert par le suivant et l’absence ne se verrait jamais ; et le contrôle de volume zero est allumé, parce que le passage annonce ce qu’il a TRAITÉ. Un passage qui bat « terminé » sans rien avoir traité — liste épuisée, configuration effacée, stock vide — lève une alerte, exactement comme chez n’importe quel client. Détail qui aurait mordu : createJob rend la tâche existante sans la modifier quand la clé d’idempotence a déjà servi, ce qui est le bon comportement (« c’est le même appel » n’est pas « voici une correction ») — mais un réglage changé dans le script n’atteignait alors jamais la tâche posée. La correction est explicite.
Six passages par jour, une réserve qui se regarnit seule, et l’arithmétique qui les cale.
Rendement mesuré : 2,8 prospects pour 100 domaines visités — 26 sur 934. Le premier chiffre qu’on avait, 6 %, venait d’un lot de 134 : dix fois moins de matière, et deux fois plus optimiste, ce qui est exactement le genre d’écart qui fait promettre des volumes qu’on ne tient pas. Un passage en visite huit cents, soit environ une heure. L’objectif n’est pas libre : il vaut le quart du palier de chauffe, sinon on grille l’adresse que tous les clients partagent. La chauffe double presque chaque jour — 200 demain, 400, 800, 1 500 — donc 50 messages demain, puis 100, 200, 375. Six passages étalés de 8 h 40 à 18 h 40 rendent environ 135 prospects par jour : de quoi suivre la chauffe jusqu’au palier de 400, et étaler l’envoi sur la journée vaut mieux pour la délivrabilité que de vider le quota du jour d’un coup. Au-delà, la limite est connue et chiffrée. Restait la réserve : à 4 800 domaines par jour, les 15 619 qualifiés durent trois jours, et une réserve qu’il faut regarnir à la main n’est pas une réserve, c’est un compte à rebours. Un passage nocturne tire donc 30 000 domaines de plus dans l’open data AFNIC et les filtre — mais seulement quand l’avance descend sous deux jours : le reste du temps il dit ce qu’il voit et s’en va, parce que résoudre trente mille domaines pour rien serait grossier. Les nouveaux sont ajoutés à la fin et le curseur continue d’avancer : réécrire le fichier ferait frapper une deuxième fois à des milliers de portes.
La prospection part toute seule chaque matin — et elle annonce le jour où sa liste s’épuisera.
Les premiers messages sont partis parce que quelqu’un a tapé une commande. Une campagne qui dépend de ça s’arrête le premier jour où personne n’y pense, et personne ne s’en aperçoit — exactement la panne que ce produit existe pour détecter chez les autres. Un passage quotidien réalimente donc la file avant d’envoyer, à 9 h 40 heure de Paris : un message froid qui arrive à trois heures du matin ressemble à ce qu’il n’est pas. Il reste éteint tant qu’il n’est pas configuré — un service installé par défaut qui se mettrait à écrire à des inconnus le jour d’un déploiement serait indéfendable, et le fichier de configuration est la seule chose qui distingue « installé » de « allumé ». Les bornes ne changent pas : le quart du palier de chauffe, le budget consulté avant d’écrire, pas de constat pas de message. Les deux fins de course s’annoncent plutôt que de se deviner : quand la liste de candidats est épuisée, il le dit TANT QU’IL RESTE DU STOCK — pas le matin où il n’y a plus rien à envoyer, ce qui serait trop tard pour en constituer une autre sans interrompre la campagne ; et quand le stock suffit, il dit pourquoi il ne fait rien, parce que ne rien faire en silence ressemble à une panne. Le curseur avance même quand un lot n’a rien donné : un domaine qui n’a pas répondu ce matin ne répondra pas davantage demain, et y revenir ferait frapper chaque jour chez les mêmes gens. La liste qualifiée est passée de 454 à 15 619 domaines — le filtre DNS n’avait été passé que sur 600 des 20 000 candidats tirés de l’open data AFNIC.
Les trois premiers messages de prospection sont partis — et le worker n’en disait pas un mot.
Ils ont été mesurés, écrits, mis en file et livrés : frenchworks.fr, vistacom.fr, whities.fr, sur un constat DMARC vérifié chez chacun. La seule trace dans le journal du worker était un passage de 9,6 secondes au lieu des 300 millisecondes habituelles. Rien ne disait qu’un courrier avait été remis, ni à combien de destinataires, ni que l’un des trois avait demandé deux tentatives — il a fallu aller lire la base pour l’apprendre. expedier rendait pourtant son bilan ; le worker le jetait sur place. Un passage trente fois plus long que d’habitude, sans un mot sur ce qu’il a fait, est exactement ce que ce produit existe pour détecter chez les autres, et on ne peut pas le laisser chez nous. Le bilan du tick porte désormais courrier: { envoyes, echoues, reportes }, et le journal s’écrit dès qu’une de ces trois valeurs bouge. Les zéros sont annoncés aussi : « la file était vide » doit se distinguer de « personne n’a regardé la file ».
Une mairie, une école et des pompes funèbres dans la liste : quinze prospects sur dix-huit retirés.
Avant d’envoyer le premier message, les dix-huit prospects collectés ont été relus un par un, en allant lire le titre de chaque site. Dix étaient à côté : la mairie de Saizerais, un ensemble scolaire, une psychologue du travail, un chirurgien ophtalmologiste, des pompes funèbres, un club de plongée, un cabinet comptable, une auto-école. Trois causes, chacune vérifiée sur la page qui l’a produite. 1. woocommerce était lu dans la FEUILLE DE STYLE : les thèmes WordPress embarquent les règles .woocommerce … li.product .price qu’on ait la boutique ou non, et la mairie ne vend rien. On ne lit donc plus ni le style, ni le script, ni les commentaires — rien de tout cela n’est montré à qui visite le site. Ce qui décide d’une boutique, c’est un panier, une caisse ou un catalogue chez elle ; la plateforme ne fait plus que nommer le constat. 2. « agence de communication » était lu dans le CRÉDIT DE PIED DE PAGE : « © 2018 Themis Medica agence de communication santé », sur le site d’un ophtalmologiste. C’est l’agence QUI A FAIT le site — le signal exactement à l’envers. Les crédits et le texte des liens qui sortent du domaine sont désormais retirés avant lecture, mais seulement sur les tournures qui ATTRIBUENT (« réalisation : », « réalisé par », « © ») : « conception et développement web » sur la page d’accueil d’une agence reste ce qu’elle dit d’elle-même. 3. /app était lu dans href="https://app.multiscreenstore.com/script.js", chez les pompes funèbres : un chemin qui n’est pas chez eux ne dit rien sur eux, donc seuls comptent les liens relatifs et ceux qui restent sur le domaine. Restaient deux erreurs après ces trois corrections — le club de plongée gardé sur son espace adhérent, l’ophtalmologiste sur sa page de tarifs d’opération. Une page /tarifs et un /connexion sont vrais chez un service en ligne ; ils le sont aussi chez eux. Pris ensemble, ils distinguent ; pris seuls, non. Une documentation d’API, elle, suffit : personne n’en publie sans avoir quelque chose qui tourne derrière. Bilan mesuré : trois prospects gardés sur dix-huit, et les trois sont justes. On perd au passage une agence bien réelle dont le site ne nomme jamais son métier — c’est le prix, et c’est le bon sens du doute : ce qui n’est pas segmenté ne reçoit rien.
Le message de prospection était replié à deux largeurs différentes.
Vu en relisant les premiers messages réels avant de les envoyer : le constat tenait en 78 colonnes, le bloc qui présente le produit en 93, parce que celui-là était replié à la main dans le code. Un message en texte brut n’a que sa mise en page pour paraître écrit plutôt qu’assemblé, et deux largeurs dans le même message se voient avant qu’on en ait lu la première ligne. Tout passe désormais par la même fonction, et le test porte sur le message ENTIER plutôt que sur le bloc corrigé — le prochain paragraphe ajouté à la main tombera dessus tout seul. Les URL sont exemptées : couper le lien de désabonnement en deux transformerait « je ne veux plus rien recevoir » en signalement pour pourriel.
Une auto-école classée « agence web » : le segment se resserre.
Premier passage de la segmentation sur sept prospects réels, et deux faux positifs : une auto-école et une société de gardiennage rangées parmi les agences. La cause tient en deux mots — « nos clients », qui figure sur à peu près tous les sites du web français, et « nos réalisations », qui figure sur ceux de tous les artisans. Un faux positif ne coûte pas qu’un message inutile : il coûte un message qui dit à une auto-école qu’elle gère un parc de sites. Celui qui le lit ne conclut pas qu’on s’est trompé de liste — il conclut que personne n’a lu son site, et il a raison. Les marques retenues sont désormais celles qui NOMMENT le métier (« agence web », « création de sites », « développement web »), et « portfolio » ne compte que s’il parle de sites — un photographe en a un aussi. Les boutiques, elles, se reconnaissent sans ambiguïté : WooCommerce, Shopify, PrestaShop laissent des traces qui ne mentent pas.
Un test du certificat échouait un jour sur deux, à une milliseconde près.
Il vérifiait qu’un certificat expirant dans vingt-trois jours le dit en jours et non en date brute. L’analyseur compte les jours restants en arrondissant vers le bas, et il le fait quelques millisecondes APRÈS que le test a lu l’heure : « exactement 23 jours » devenait 22,99999 jour, puis 22. Le test échouait alors sans qu’une ligne de code n’ait bougé, et seulement parfois — le pire des deux mondes. Une heure de marge met la valeur au milieu de sa journée plutôt qu’à sa frontière, et ne change aucun des seuils éprouvés : trois jours restent sérieux, vingt restent une attention.
La collecte a rendu le « contact@ » d’un médecin. Le message aurait été exact, et mauvais.
La première vraie collecte a rendu dix-huit adresses parfaitement valides : contact@ d’un cabinet médical, d’une thérapeute, d’une pompe funèbre. Le constat mesuré chez eux était vrai — leur domaine est usurpable, leur site répond en clair — et le message aurait été exact de bout en bout. Il aurait quand même été mauvais. Un médecin n’a pas de tâche planifiée qui répond « succeeded » sans rien faire, pas de file qui se vide, pas d’agent qui déploie. Lui écrire « laisse ton IA tout gérer » ne convertit pas, et le peu qu’on y gagnerait se paierait en signalements sur une adresse d’expédition partagée par tous nos clients. La règle est donc devenue symétrique de celle du constat : pas de segment reconnu, pas de prospect. Les deux refusent d’écrire — l’une quand on n’a rien à dire, l’autre quand on n’a rien à proposer. Et on ne devine pas un métier à partir d’un nom : pascaleglardon.fr peut être une consultante en infrastructure. On lit ce que le site MONTRE — une plateforme de commerce, un portfolio d’agence, une page de tarifs, une documentation d’API — parce que ce sont des faits, et qu’ils répondent exactement à la question qui compte : cette organisation fait-elle tourner quelque chose qui peut tomber en silence ? La source enregistrée porte désormais le segment ET ce qui l’a décidé : le jour où quelqu’un demande pourquoi on lui a écrit, la réponse tient en une ligne.
Huit sites français sur douze ne répondent pas en HTTPS. En 2026.
Mesuré en cherchant pourquoi la collecte ne trouvait aucune adresse : zéro sur quarante domaines, ce qui est trop beau pour être vrai. Le défaut n’était pas chez eux. Sur douze domaines tirés au hasard — actifs, vieux de plus de deux ans, avec un MX — huit ne répondaient pas du tout en HTTPS mais répondaient en HTTP, et plusieurs autres redirigent l’apex vers www. Ne tenter que https://domaine.fr revenait à ne rien voir chez les deux tiers d’entre eux, puis à conclure qu’ils ne publient pas d’adresse alors qu’on n’avait pas frappé à la bonne porte. La collecte essaie donc quatre origines dans l’ordre — le chiffré d’abord, le clair en dernier, parce que s’y rabattre est une dégradation — et suit au plus deux redirections, jamais hors du domaine : suivre une redirection vers ailleurs ferait de cette collecte un moyen de faire visiter n’importe quoi par nos serveurs. De zéro, on passe à une adresse trouvée sur dix, avec sa source exacte : « publiée sur https://autoecolerichard.fr/mentions-legales ». Et ce chiffre-là est aussi un constat qu’on a le droit de leur écrire : un site qui ne répond qu’en clair est marqué « non sécurisé » par tous les navigateurs, et ses formulaires voyagent en clair.
La prospection s’annonce, et elle obéit au robots.txt — un défaut trouvé en l’écrivant.
C’est la seule de nos visites que personne ne sollicite : nous venons parce que nous avons décidé de venir. La cacher derrière le motif « veille » aurait été dire une chose et en faire une autre, ce que ce produit reproche aux autres à longueur de page. Un motif prospection existe donc, il est écrit dans l’agent, et la page du robot dit exactement ce qu’il fait : une seule visite, cinq pages publiques au plus, pour y lire l’adresse de FONCTION que le site publie lui-même — jamais une adresse nominative, jamais une adresse devinée. Et en câblant l’obéissance au robots.txt, un défaut est tombé : la vérification compare le DÉBUT de ce qu’on lui donne au nom écrit dans le fichier, et notre agent complet commence par « Mozilla/5.0 ». Un site qui écrivait User-agent: LeTockBot / Disallow: / n’était donc jamais entendu — alors que la même page promet que « bloquer ce jeton suffit à nous bloquer partout ». La promesse était fausse ; elle est maintenant tenue, et un test la garde, pour le blocage nominatif comme pour le User-agent: * — celui-là compte plus encore : un site qui écrit « * » n’a pas pensé à nous, et c’est justement pour ça qu’il faut l’écouter.
La page qui explique notre robot se trompait sur notre robot.
Elle affirmait qu’« un robot ne passe sur un domaine que parce que quelqu’un l’a déclaré dans son compte ». C’était inexact depuis l’ouverture de /verifier : cette page est publique, sans compte et sans preuve, et elle audite le domaine qu’on lui donne — donc notre robot visite des domaines que personne n’a déclarés, à la demande de n’importe qui. Une page qui explique un robot ne peut pas se tromper sur ce que fait ce robot : c’est la seule page que lira l’administrateur qui nous voit dans ses journaux, et si elle ne correspond pas à ce qu’il observe, il en conclut — à raison — qu’il ne peut se fier à rien de ce qu’on écrit. Les deux cas sont maintenant distingués : la surveillance récurrente exige la déclaration ET la preuve de possession ; une vérification ponctuelle peut être demandée par n’importe qui, sur n’importe quel domaine, et elle ne se répète pas d’elle-même. « Si tu nous vois passer une fois sans avoir rien déclaré, c’est probablement que quelqu’un s’est posé une question sur ton site. »
Le test du coffre : la correction d’hier ne corrigeait rien.
Il fabriquait une altération en remplaçant les deux derniers caractères du chiffré par « AA », et échouait quand le chiffré finissait déjà ainsi. La correction changeait le dernier caractère pour un autre — et elle était fausse pour une raison plus intéressante que la première : en base64, quatre caractères portent trois octets, et quand le dernier groupe est incomplet, les bits de bourrage du dernier caractère ne sont pas décodés. « QQ== » et « QR== » rendent le même octet. La valeur « abîmée » différait donc de l’originale sans que le contenu chiffré change d’un bit, et le déchiffrement réussissait — le test continuait à rougir, moins souvent. C’est le premier caractère qu’on modifie désormais : il porte toujours six bits utiles du premier octet. Un test qui garde la détection d’altération d’un coffre à secrets est le dernier endroit où l’on veut d’un « ça passe la plupart du temps ».
Une tâche qui manque la moitié de ses occurrences n’est pas en panne — son planning ment.
Le pendant du signal précédent, et il attrape l’inverse : celle qui bat TOUJOURS, et qui manque quand même une occurrence sur deux — parce que son planning attend un battement tous les quarts d’heure alors qu’il en arrive un toutes les trois quarts d’heure. Rien n’est en panne : le service tourne, les battements arrivent, et l’écran est rouge une fois sur deux. C’est pire qu’un rouge franc : on apprend que ce rouge-là ne veut rien dire, puis on l’apprend pour les autres. Le signal ne dit donc pas « répare » — il n’y a rien à réparer — il donne les deux chiffres : ce que le planning annonce, et ce qui bat vraiment, tous deux mesurés comme des écarts médians plutôt que lus dans l’expression cron (une expression se lit mal, un écart se mesure). Le geste est de corriger le planning, ou d’allonger le délai de grâce si les battements sont simplement irréguliers. Le seuil — un quart des occurrences manquées — vient d’une mesure sur toute la plateforme : une tâche à 56 %, toutes les autres entre 0 et 3 %.
Un test du coffre à secrets rougissait une fois sur quelques milliers.
Il vérifiait qu’un secret modifié est refusé au lieu de rendre des ordures — et il fabriquait la modification en remplaçant les deux derniers caractères par « AA ». Une fois sur quelques milliers, le chiffré finissait DÉJÀ par « AA » : la valeur « abîmée » était identique à l’originale, le déchiffrement réussissait, et le test échouait sans qu’aucune ligne de code n’ait bougé. C’est la troisième fois cette semaine qu’on trouve ce défaut sous une forme différente, et la leçon est toujours la même : un test qui rougit au hasard apprend à ignorer le rouge. Celui-ci garde la détection d’altération d’un coffre à secrets — c’est le dernier endroit où l’on veut prendre cette habitude. La modification change désormais le dernier caractère pour un AUTRE, choisi en fonction de lui, et le test vérifie d’abord qu’il a bien modifié quelque chose.
La collecte des prospects ne devine aucune adresse, et dit où elle a lu chacune.
La campagne de prospection avait son moteur — mesurer, choisir un constat, écrire, cadencer — et pas sa liste. La pièce qui manquait n’est pas un fichier : c’est la règle de collecte. On ne devine pas une adresse, on ne la compose pas à partir d’un nom de domaine, on ne l’achète pas. On lit celle que le site publie lui-même, sur ses propres pages, et on note où et quand. Trois raisons, et la troisième est la seule qui tienne sur la durée : une adresse devinée rebondit, et chaque rebond abîme une réputation d’expédition partagée par tous nos clients ; une adresse achetée est indéfendable le jour où quelqu’un demande d’où elle vient, et quelqu’un demande toujours ; une adresse publiée sur une page « Contact » est une invitation à écrire — c’est ce qui sépare un message qu’on a le droit d’envoyer d’un message qu’on s’autorise. Seules les adresses de FONCTION sont retenues (contact@, info@, hello@) : une adresse nominative désigne une personne, et le régime n’est pas le même. Les adresses techniques — postmaster@, abuse@, dpo@ — sont écartées d’office : y envoyer de la prospection est la meilleure façon de se faire signaler par celui qui traite les signalements. Un domaine qui ne publie rien est un domaine qu’on laisse.
Une tâche tue depuis huit jours produisait 173 occurrences manquées en 48 heures.
Trouvé en appliquant nos propres règles à notre propre compte. Une tâche en mode « monitor » qui cesse de battre lève une alerte de silence une fois — c’est la bonne règle, sinon on réveille quelqu’un tous les quarts d’heure pour la même nouvelle. Mais ensuite plus rien ne se passe, et la tâche continue d’exister. Trois conséquences, et la troisième est la seule qui compte vraiment : elle occupe un quota qu’on paie, elle grossit la table des exécutions d’une ligne tous les quarts d’heure indéfiniment, et surtout elle laisse un rouge permanent dans l’écran. Un écran où quelque chose est rouge depuis huit jours apprend à ne plus regarder le rouge — et le jour où une vraie panne s’affiche, elle se range dans le décor. C’est le même raisonnement que pour un test qui échoue au hasard. Un signal — pas une alerte, personne n’est en panne — s’ouvre au bout de huit jours de silence et nomme les deux gestes possibles : archiver (rien de l’histoire n’est perdu, et la tâche ne compte plus dans le quota) ou réparer le battement. On ne met pas en pause tout seul : ce serait décider à la place de quelqu’un sur une tâche qui peut revenir demain, et une surveillance qui s’éteint d’elle-même est exactement le genre de service qu’on ne peut plus croire. Le signal se referme seul dès que la tâche se remet à battre — un signal qui ne s’éteint jamais devient un décor, ce qui est très exactement le défaut qu’il dénonce. Il ne parle ni d’une tâche jamais branchée (autre nouvelle, autre geste), ni d’une tâche en pause ou archivée : dans ces deux cas la personne a déjà tranché, et lui redemander serait le contraire d’un service.
La fonction qui pose vraiment les tâches n’avait aucun test contre la base.
Tout le module des recettes était éprouvé — le choix d’un constat, la vérification d’un partage, le rendu d’un message — sauf la seule fonction dont le résultat existe ailleurs que dans sa valeur de retour : celle qui CRÉE les tâches. Ce qu’on veut savoir d’elle est ce qui existe après l’appel, et ça ne se vérifie qu’en regardant la base. Sept tests désormais, dont celui qui compte le plus : poser deux fois la même recette rend les MÊMES tâches. Un agent qui n’a pas reçu la réponse recommence — s’il en sortait des doublons, ils alerteraient en double et on ne saurait plus laquelle croire. Au passage, la fonction accepte un résolveur DNS injectable, comme celle qui déclare un crochet : une adresse posée par une recette passe par le garde anti-SSRF, et sur un poste dont la résolution est filtrée, tous ces tests échouaient sur « URL refusée » — un défaut qui ressemble à une régression du code alors qu’il n’en est pas une.
« Relay access denied » faisait supprimer des adresses qui existent.
Trouvé en regardant notre propre trafic du jour. Ce motif vivait dans la famille des « adresses inconnues » : il produisait donc un rebond DUR, donc une suppression définitive du point de vue de l’expéditeur. Or il ne dit pas que la boîte n’existe pas — il dit que le serveur qu’on a joint refuse de relayer pour ce domaine, c’est-à-dire que son MX pointe vers une machine qui n’accepte pas son courrier. La boîte peut très bien exister, et elle existera encore quand le DNS sera réparé. Deux adresses supprimées ainsi en une journée, dont une dont le domaine portait un « www. » de trop : une faute de collecte, pas une adresse morte. C’est exactement la classe de défaut que le champ cause a été introduit pour empêcher, et elle s’était glissée dans le motif d’à côté. Elle devient un refus : signalée, jamais supprimée — le doute ne supprime pas. Et la règle tranche désormais AVANT les codes, pour couvrir aussi les serveurs qui rendent « 550 relaying denied » sans code étendu.
Un expéditeur refusé était classé « incident réseau ».
Sender address rejected: Domain not found pour un domaine d’envoi sans DNS était rendu avec cause: transport — c’est-à-dire « un incident passager, ça repartira ». Ça ne repartira pas : c’est le domaine d’envoi qui n’existe pas dans le DNS, et la boucle de reprise le réessayait indéfiniment sans aucune chance d’aboutir. Pendant ce temps, le client cherchait du côté du destinataire. La cause devient expediteur, qui change l’endroit où l’on va regarder ; l’issue reste un différé, parce qu’un 4.x.x se réessaie. Même chose pour « Emetteur invalide / Invalid Sender », que les serveurs d’Orange rendent en français.
L’exploitation dit depuis quand plus rien ne parle de nous.
Une adresse d’expédition classée se répare, puis on attend. Pendant cette attente, rien à l’écran ne distinguait « c’est réglé » de « ça n’a pas encore recommencé » — et les deux appellent des conduites opposées : reprendre le volume, ou ne surtout pas y toucher. Le temps écoulé depuis le dernier refus dont la cause est l’expéditeur tranche. Il monte tout seul quand ça va bien, il retombe à zéro à la seconde où ça recommence, et il passe en alerte s’il date de moins de six heures. C’est aussi la seule mesure honnête de notre réputation : les listes qui comptent refusent les résolveurs publics et répondent 127.255.255.x, qui veut dire « je refuse ta requête » et non « cette adresse est inscrite » — mais le serveur qui refuse un message, lui, le dit, et sa mesure est datée et prouvée par un courrier qui n’est pas arrivé.
Le message de prospection vendait une configuration là où il n’y a rien à configurer.
Il proposait d’installer une recette pour surveiller le constat qu’il venait de rendre — et c’était faux, de la pire façon : faux en notre défaveur. Le constat vient d’une sonde de la Veille, et cette sonde tourne toute seule sur chaque domaine déclaré. La phrase qui ouvre le message a été produite par elle. Proposer de l’installer laissait croire qu’elle demandait un travail qu’elle ne demande pas. Le message dit donc d’abord la vérité — « ce constat vient d’une sonde qui tourne toute seule, tu n’as rien à configurer pour qu’elle te prévienne le jour où ça recasse » — et ne propose une recette qu’ensuite, uniquement pour ce qu’une sonde ne peut pas voir depuis dehors : lire les rapports DMARC, vérifier qu’un formulaire de contact part vraiment. Présenter les deux comme interchangeables reviendrait à vendre deux fois la même chose, et celui qui s’en aperçoit doute de tout le reste. Un test tient l’ordre des deux phrases.
Treize codes d’erreur sortaient de l’authentification sans exister nulle part.
Pour qui consomme l’API, un code non documenté est un code MUET : on le reçoit, et il n’y a aucun endroit où apprendre s’il faut réessayer, corriger la requête ou renoncer. Treize manquaient, tous du côté de l’authentification et des modèles de courrier. Six d’entre eux sont imposés mot pour mot par la RFC 6749 — invalid_request, invalid_client, invalid_grant, unsupported_response_type — et c’est dit, parce qu’une bibliothèque OAuth cliente les reconnaît et agit dessus sans lire le message : les traduire casserait ce comportement. Le catalogue explique aussi pourquoi un code d’autorisation ne sert qu’une fois (le rejouer est le signe qu’il a été capté) et pourquoi la comparaison des adresses de redirection est exacte (accepter un préfixe laisserait détourner le code vers un chemin choisi par l’attaquant).
Le relevé des codes tenait une liste à la main, et se trompait dans les deux sens.
Le test qui vérifie ce catalogue lit les sources. Il ne connaissait qu’une famille de classes d’erreur, tenue à la main — et une liste se périme : une classe née un matin rendait ses codes fantômes le jour même, sans qu’aucun test ne rougisse. Élargi à tout ce qui finit par « Error », il faisait pire : ForumError ne porte PAS de code, et le relevé attrapait alors les mots qui traînaient dans les parages — annoncer, guide, epingler — qui sont des PERMISSIONS. Il exigeait de les documenter, et un test qui demande l’impossible finit désactivé. Il lit désormais la source pour savoir quelles classes déclarent un code : une classe nouvelle est prise en compte le jour où elle est écrite, une classe sans code ne fait aucun bruit. Et le motif a gagné une virgule : un code est un ARGUMENT, pas une chaîne qui traîne — sans elle, p.get(« scope ») et p.get(« redirect_uri ») étaient comptés comme des codes d’erreur, et documenter ce qui n’existe pas est le meilleur moyen de faire perdre confiance au catalogue.
Poser une recette d’un clic, et une recette publiée qui menait à un 404.
Le catalogue est maintenant dans la barre du module Cron — pas seulement derrière la page vide, parce qu’on ne part pas d’une recette uniquement la première fois : la deuxième sauvegarde, le premier export comptable, la file qu’on branche six mois plus tard passent par le même endroit. Chaque carte porte le piège, ce qu’elle pose, et le bouton qui la pose. Les champs sont DANS la carte : les mettre derrière un second écran ajouterait un pas entre « je comprends ce que ça fait » et « je le fais », et c’est très exactement le pas où l’on abandonne. L’API reste la bonne réponse pour un agent ; elle n’en est pas une pour quelqu’un qui découvre le produit et regarde une liste vide — lui dire « fais un POST avec ta clé » revient à lui demander d’écrire du code pour avoir le droit d’essayer. Les deux chemins passent par la même fonction : un écran qui appellerait un chemin à lui finirait par se comporter autrement, et c’est toujours l’écran qui a raison aux yeux de celui qui regarde. Et un défaut qui aurait été gênant : la page publique d’une recette ne connaissait que les officielles, figées à la construction. Une recette publiée menait donc à un 404 — et l’adresse de ce 404 était celle que la publication venait de rendre à son auteur. Une page annoncée qui n’existe pas est pire qu’une page absente : elle fait douter du reste. La notice a sa section « recettes », parce qu’un agent qui arrive sur un projet neuf ne cherche pas à comprendre le produit — il cherche quelque chose d’installable, et sans elle il lit quinze mille mots puis invente sa propre surveillance en refaisant les erreurs que d’autres ont déjà payées.
Une recette peut être publiée pour les autres — et le forum en est la relecture.
POST /api/v1/recettes/publier ouvre, du même geste, la recette et le fil qui la relit. Ce fil n’est pas une annonce : il est la revue, public et permanent, lu par ceux qui vont l’installer. On n’a pas inventé de file de modération — le lieu existait, avec ses votes, ses réponses, ses signalements et sa réputation ; en créer un second aurait produit deux endroits où regarder, donc un endroit que personne ne regarde. Deux fautes sont refusées AVANT d’écrire quoi que ce soit, parce qu’aucune ne se rattrape : une adresse laissée en dur ferait appeler le serveur de son auteur par toutes les équipes qui la posent — chacune ne voyant que sa propre tâche — et une clé oubliée dans un en-tête serait publique et permanente, donc à révoquer et non à retirer. Le refus liste TOUT ce qui cloche d’un coup : un aller-retour par défaut fait abandonner, et on perd une recette qui aurait servi. Une recette passe « éprouvée » quand une autre équipe que son auteur la pose — c’est la seule preuve qui vaille, parce qu’une recette qui ne marche que chez celui qui l’a écrite est exactement ce qu’une vérification automatique ne peut pas attraper. recettes.publier est une mission à part de cron.ecrire : poser crée des tâches chez soi, publier propose quelque chose qui s’exécutera ailleurs. Et une collision évitée de justesse : le code d’erreur cle_invalide désignait déjà une clé d’API refusée, en 401 — un même mot pour deux sens envoie chercher au mauvais endroit.
Un test de parallélisme déduisait d’un chronomètre ce qu’il pouvait compter.
Il posait cinq tâches de trois cents millisecondes et vérifiait que le tout tenait en moins de 1,2 seconde : en série il en faudrait 1,5. Le raisonnement est juste, la mesure ne l’est pas — sur une machine chargée, et la suite entière lance deux cents processus, la seule attente d’ordonnancement suffit à franchir le seuil. Le test rougissait alors sans qu’aucune ligne de code n’ait changé. Un test qui rougit au hasard apprend à ignorer le rouge, et c’est le pire service qu’il puisse rendre : le jour où la boucle repasse vraiment en série, on relancera « parce qu’il flotte ». Il compte désormais les appels EN VOL au même instant — en série, jamais plus d’un. C’est indépendant de la charge, et c’est une preuve au lieu d’un indice.
« Refusés à la porte : 11 » additionnait deux choses qui disent le contraire.
L’écran d’exploitation annonçait ce total avec pour toute explication « adresse non déclarée, trop lourd, quota — c’est le contrat ». Quelqu’un l’a lu et a posé la seule question qui compte : « tous nos messages entrants sont refusés ? » Mesuré : cinq tentatives de RELAIS depuis une adresse usurpant notre propre domaine (« support@letock.fr » → « info@ctalab.net »), quatre passages d’une sonde d’open-relay connue depuis quatre IP, et deux essais internes. Aucun courrier légitime. Sept sur onze étaient des robots qui essayaient de se servir de nous pour écrire à des inconnus — et les refuser est une bonne nouvelle, pas une panne. Mais le compteur rangeait sous le même mot deux situations qui demandent des gestes opposés : « le destinataire n’est pas chez nous » veut dire qu’on bloque un relais et qu’il n’y a rien à faire ; « le destinataire EST chez nous, mais l’adresse n’est pas ouverte » veut dire qu’un courrier s’est PERDU, que quelqu’un a écrit et que personne ne l’a lu. Additionnés, le second se noie dans le premier — et le jour où un vrai message se perd, il disparaît dans le bruit des robots, qui est très exactement le mode de panne que ce produit existe pour attraper. Trois chiffres désormais, et chacun dit ce qu’il faut en faire : « relais refusés », « écrit à une adresse fermée », et les « différés » du quota, qui ne sont pas des refus du tout — un 452 est temporaire, le serveur d’en face garde le message et le représente pendant des jours. Migration 101 : les onze lignes déjà enregistrées ont été requalifiées, sinon l’écran aurait annoncé onze courriers perdus, c’est-à-dire l’inverse de la vérité.
Les recettes s’installent en un appel, et se lisent sans compte.
Treize configurations prêtes à poser, chacune avec le piège qu’elle évite — la seule partie qui ne se devine pas. POST /api/v1/recettes/{clé}/poser crée les tâches d’un coup : le moment où l’on abandonne n’est pas la lecture d’une recette, elle est courte, c’est l’étape d’après — ouvrir cinq écrans, recopier un cron, choisir un mode, deviner un délai de grâce, recommencer quatre fois. L’appel est rejouable : chaque tâche porte une clé d’idempotence dérivée de la recette, donc deux appels identiques rendent les MÊMES tâches. Un agent qui n’a pas reçu la réponse n’a qu’une conduite raisonnable — recommencer — et il ne doit pas en sortir des doublons qui alerteront en double. Poser une recette exige cron.ecrire, exactement comme créer une tâche : ce qu’on obtient est identique, seul le chemin change, et sans cette règle le catalogue serait une porte dérobée vers le module qu’il configure. Un test le tient. La page /recettes est publique : demander de se connecter pour LIRE ce qu’on propose de surveiller reviendrait à faire payer avant de dire ce qu’on vend. Et une correction que nos propres recettes imposaient : elles déclaraient une adresse à remplir dont aucune tâche ne se servait — en mode « monitor », LeTock n’appelle rien, il attend un battement. Un champ à remplir pour rien apprend à remplir sans lire ; c’est désormais refusé à la publication, pour nous comme pour les autres.
La chauffe dit désormais ta part, et la réputation partagée s’annonce avant la bascule.
Deux demandes du même client, la même nuit, et les deux étaient justes. ta_part : la route du budget disait ce qu’il RESTE du palier du jour, sans dire qui l’avait pris. « Si c’est moi qui ai rempli le palier, je ralentis et c’est efficace ; si c’est un voisin, ralentir ne change rien et je n’ai qu’à attendre demain. » Sans ce chiffre il retenait ses envois dans les deux cas — dont un où c’était inutile — et il avait écrit à son client que le volume du jour venait de ses autres produits, ce qu’il ne pouvait pas savoir. Son propre volume est son chiffre : le lui rendre n’apprend rien sur personne. C’est la limite exacte de ce qu’on publie sur une ressource partagée — ce qui reste, et ta part, jamais celle d’un autre ; un test le tient équipe contre équipe. Et l’avertissement sur la réputation commune vivait dans un encadré au milieu de la section Mail : « quelqu’un qui lit “Démarrer en 30 secondes”, pose ses deux TXT et bascule ses cinq cents courriers quotidiens la découvre après, c’est-à-dire au pire moment, et il aura entre-temps dépensé la réputation de tout le monde ». Ce n’était pas un choix, c’était un oubli. Ce fait ne change pas la FAÇON de basculer, il change la DÉCISION de basculer : il est donc rendu par POST /api/v1/mails/domaines, à l’instant où l’on prépare un domaine, avec le plancher qui garantit que les premiers messages du jour partent quoi qu’aient fait les autres.
Un détail vous a gêné, ou vous a coûté du temps ? Répondez ici plutôt
que par courrier : la réponse servira aussi aux suivants.