J’ai une app de barre de menu qui a besoin d’obtenir un chiffre. Un pourcentage entre 0 et 100. Pour ce faire, elle interroge un serveur toutes les 30 secondes.

Faites le calcul : 30 secondes ça fait 2 requêtes par minute, 120 par heure, 960 sur une journée de travail de 8 heures. Près de mille requêtes HTTP par jour pour lire un chiffre qui parfois ne change pas pendant 20 minutes.

Ce n’est pas de la surveillance. C’est du harcèlement envers le serveur.

Le vrai problème n’est pas technique. Il est politique.

Quand on dépend d’une API qu’on ne contrôle pas — qui n’est pas publique, qui n’a pas de limites de taux documentées, qui appartient à une entreprise pouvant changer ses Conditions d’Utilisation n’importe quel mardi — chaque requête inutile est un risque. Pas un risque de timeout. Un risque de vous voir fermer les accès.

Le endpoint que j’utilise n’est pas documenté. Il fonctionne aujourd’hui. Il fonctionne depuis des mois. Mais chaque requête que j’envoie devient une ligne de plus dans un log qu’un employé chez Anthropic pourrait consulter pour se dire qu’une app tierce fait trop de bruit.

Donc la vraie question n’est pas “comment faire du polling plus rapide ?” mais “comment faire du polling le moins possible sans perdre d’information ?”

Et là, mon ami, c’est là qu’un ingénieur raisonnable écrirait un if et qu’un ingénieur avec un plaisir coupable pour la sur-ingénierie créerait un filtre de Kalman.

La solution naïve (et pourquoi elle échoue)

La première idée est toute simple : si le chiffre n’a pas changé, ne demande pas.

si valeur_actuelle == valeur_précédente :
    attends plus longtemps
sinon :
    reviens à 30 secondes

Ça fonctionne très mal. Le chiffre change quand toi tu fais quelque chose (envois de messages, utilisation de jetons). Mais il change également quand tu ne fais rien — la limite a une fenêtre glissante de 5 heures, donc les jetons anciens expirent automatiquement. Et si tu utilises le service sur un autre appareil, le chiffre monte sans que tu le saches.

Il ne suffit pas de regarder s’il a changé. Il faut prédire quand il pourrait changer et avec quelle confiance.

Kalman pour les pressés

Un filtre de Kalman est un moyen de combiner deux sources d’information imparfaites.

Imagine que tu es dans une pièce sans fenêtres et que tu veux connaître la température extérieure. Tu as deux options :

  1. Ton modèle mental : “Il est 15h en mars à Genève, donc j’estime environ 10°C”. C’est une estimation raisonnable, mais pas parfaite — il pourrait pleuvoir ou y avoir du vent.
  2. Un thermomètre bruyant : tu peux aller sur le balcon, mais ton thermomètre bon marché fluctue de ±3°C.

Aucune source n’est parfaite. Le filtre de Kalman dit : combine les deux, mais donne plus de poids à celle qui est la plus fiable à ce moment précis.

Si tu viens de regarder le thermomètre il y a 10 secondes, ton modèle mental est excellent — fais-lui confiance et ne t’embête pas à vérifier à nouveau. Si cela fait une heure, ton modèle s’est dégradé — va sur le balcon.

La clé est la variance : un chiffre qui mesure “à quel point je fais confiance à mon estimation actuelle”. Elle commence à zéro juste après avoir vérifié le thermomètre, et augmente avec le temps. Quand elle dépasse un seuil, le filtre dit “je ne fais plus confiance, j’ai besoin d’un vrai chiffre.”

Dans mon cas :

  • Le modèle mental = coût local des jetons. Je sais combien j’ai consommé dans Claude Code, donc je peux calculer combien devrait avoir augmenté la limite.
  • Le thermomètre = l’API d’Anthropic. Une donnée réelle, mais chaque lecture a un coût politique et énergétique.
  • La variance = incertitude qui augmente avec le temps. Si j’utilise le service depuis le navigateur ou mon téléphone, mon modèle local ne le sait pas — et cela dégrade la prédiction.

Un filtre Kalman complet (multidimensionnel, avec matrices de covariance) serait tirer au canon sur une mouche. Ce que j’ai implémenté est un filtre scalaire : un seul état (utilisation), un seul capteur (l’API), un modèle linéaire (coût/budget). 20 lignes de code. La version minimale qui règle le problème.

Traduit à mon cas concret :

  • Prédiction : utilisation_estimée = dernier_donne_réel + (coût_local_nouveau / budget) × 100
  • Correction : à chaque réponse du serveur, le filtre réinitialise sa variance à zéro.
  • Incertitude : la variance augmente linéairement avec le temps. σ = √(Q × secondes_depuis_dernière_correction).

La nouveauté : le filtre décide quand questionner

Voici où la sur-ingénierie se justifie. Le filtre n’est pas seulement une estimation du chiffre — il décide quand une donnée réelle est nécessaire. Cinq règles, évaluées à chaque tick :

RègleDéclencheurPourquoi
Fenêtre réinitialiséenow ≥ resetsAtLes jetons ont expiré. Les données précédentes ne sont plus valides.
Incertitude élevéeσ > 5%Je ne fais plus confiance à ma prédiction.
Franchissement de frontièreL’intervalle de confiance traverse 80 %, 95 % ou 100 %Je frôle une zone critique. L’utilisateur doit le savoir.
Proximitéutilisation à moins de 8 % d’une frontièreIl pourrait y avoir un changement non détecté (activité externe).
Timeout sécurité15 minutes sans donnée réelleParanoïa justifiée sur les systèmes de surveillance.

Si aucune règle ne se déclenche, le filtre dit “pas de souci, je gère” et l’app ne fait pas la requête HTTP. Le chiffre affiché à l’utilisateur est l’estimation locale.

Attention aux chiffres : l’estimation locale coûte zéro réseau, zéro batterie, zéro risque. C’est de l’arithmétique pure en mémoire.

Les résultats : avant et après

Une journée typique avec une limite stable (usage modéré, sans pics) :

ScénarioRequêtes/heureRequêtes/jour (8h)
Polling fixe à 30s120960
Avec estimateur bayésien15-30120-240
Estimateur + dormant4-1030-80

Cela réduit les appels réseau de 75-97 %. Pas mal pour “juste” faire des prédictions locales entre des requêtes réelles.

Mais ce n’est pas tout (la dégradation adaptative)

Le filtre de Kalman règle le problème de “quand questionner”. Mais il y a un autre niveau : combien d’effort investir pour questionner.

L’app ajuste son intervalle de polling selon le contexte :

Activité récente (< 10 min)    → 30s
Inactivité modérée (10 min - 1h) → 120s
Inactivité longue (> 1h)       → 300s
Limite > 80 %                  → Toujours 30s (zone critique)
Mode économie d'énergie        → ×2 de l’intervalle de base
Erreurs consécutives           → Backoff exponentiel (jusqu'à 5 min)

Chaque niveau demande “combien d’information est nécessaire maintenant”. Si tu ne programmes pas, pourquoi gaspiller de la batterie pour vérifier tes limites toutes les 30 secondes ? Si ton portable est à 15 % de batterie, est-ce que cela vaut la peine de faire deux fois plus de requêtes HTTP ?

Le mode dormant : quand l’app s’endort toute seule

Et voici ma partie préférée. Celle qui, soyons honnêtes, n’était peut-être pas nécessaire, mais qui apporte la satisfaction d’une solution élégante et utile.

Lorsque l’estimateur bayésien produit cinq estimations consécutives où le chiffre varie de moins de 0,5 %, l’app entre en mode dormant :

  1. Le timer s’arrête.
  2. L’estimation s’arrête.
  3. L’app surveille uniquement le filesystem.

Pourquoi le filesystem ? Car si tu utilises le service, des fichiers locaux sont générés. Quand le watcher détecte de l’activité, l’app se réveille, effectue une requête API immédiate pour s’ancrer à la réalité, puis reprend son cycle normal.

C’est comme un chien endormi près de la porte. Il ne consomme aucune énergie, mais se réveille instantanément au moindre bruit.

Le résultat : si tu arrêtes de travailler à 14h et recommences à 16h, l’app a fait zéro requêtes pendant ces deux heures. Zéro. Pas de polling toutes les 5 minutes, pas de keepalive. Le timer est littéralement inexistant. Et dès ton retour, en quelques millisecondes, l’information affichée est actualisée.

“Et ce n’était pas plus simple de faire un setInterval de 5 minutes et basta ?”

Oui. Beaucoup plus simple. Et probablement suffisant pour 90 % des utilisateurs.

Mais il y a une différence importante quand ton app tourne 8 heures par jour en arrière-plan :

setInterval(5min)Estimateur + dormant
Requêtes/jour inactif960
Requêtes/jour actif9630-80 (adaptatif)
Latence de mise à jour0-5 min< 1s (wake via FSEvent)
Consommation batterie inactifConstanteNulle
Précision dans la zone critiquePareil (5 min de délai)30s (> 80 %)

La ligne importante est la troisième. Avec un timer fixe de 5 minutes, si la limite passe de 78 % à 95 % entre deux ticks, l’utilisateur ne la voit pas avant 5 minutes. Avec un estimateur bayésien, l’intervalle descend à 10 secondes lorsque les prédictions locales indiquent une probabilité élevée, et le filtre effectue une requête réelle dès que son intervalle de confiance dépasse les 80 %.

Dit en langage clair : il réagit plus vite tout en réduisant les requêtes.

La part sérieuse : pourquoi c’est un software responsable

Mettons de côté le plaisir de la sur-ingénierie et retraçons l’objectif réel : créer quelque chose de respectueux.

Chaque requête HTTP faite par ton app en arrière-plan a un coût payé par toi, par le serveur, et par la planète. Ce n’est pas une rhétorique. C’est de la thermodynamique. Un wakeup réseau dans un portable en veille allume la radio WiFi, négocie TLS, attend une réponse, traite les données, puis se rendort. Multiplié par mille apps faisant la même chose, cela devient une des raisons pour lesquelles ton MacBook ne tient que 6 heures au lieu de 10.

Apple l’a compris. C’est pourquoi macOS inclut App Nap, Timer Coalescing, et punit les apps avec un Energy Impact élevé. Au départ, mon app avait 857 en Energy Impact. Mon objectif était de descendre à moins de 5.

L’estimateur bayésien avec mode dormant n’était pas un caprice. C’était la seule manière d’obtenir ce chiffre sans sacrifier l’expérience utilisateur. Réduire les requêtes était obligatoire. Les rendre intelligentes était le défi.

La recette, si cela peut t’aider

Si tu as une app qui fait du polling vers un serveur et veux réduire les requêtes sans perdre de réactivité :

  1. Mesure si une prédiction locale est possible. Si la donnée que tu consultes dépend d’informations aussi disponibles localement, tu peux interpoler entre les appels au serveur.

  2. Modélise l’incertitude. Prédire ne suffit pas. Il faut aussi savoir combien tu fais confiance à ta prédiction. Un filtre Kalman scalaire nécessite seulement 20 lignes.

  3. Définis des zones de décision. Où la précision est-elle cruciale ? Ne gaspille pas tes ressources dans les zones où la précision importe peu (0-60 %), mais concentre tes efforts là où elle est critique (80-100 %).

  4. Adapte au contexte. Batterie faible, longue période d’inactivité, erreurs serveur — chaque contexte a un coût différent pour une requête. Ton polling devrait le refléter.

  5. Prévoyez un mode zéro. S’il n’y a aucun signal d’activité sur l’app, ne fais rien. Absolument rien. Rien à part attendre. Un événement filesystem ou réseau te réveillera au besoin.

Le plaisir coupable

Soyons honnêtes : est-ce qu’il fallait vraiment un filtre de Kalman pour une app de barre de menu affichant un pourcentage ? Probablement pas. Quelques if et des heuristiques auraient suffi à régler 80 % du problème.

Mais les 20 % restants font toute la différence entre une app qui “fonctionne à peu près” et une app que l’on peut laisser tourner 12 heures de suite sans s’apercevoir qu’elle est là. Entre 857 en Energy Impact et moins de 5. Entre 960 requêtes par jour et 30.

Parfois, le plaisir coupable de la sur-ingénierie est exactement ce qu’il fallait pour résoudre le problème. Sauf qu’on ne le sait qu’après l’avoir réalisé.

Et si quelqu’un chez Anthropic consulte un jour les logs serveur et constate que mon app fait seulement 30 requêtes par jour au lieu de mille, j’espère qu’il pensera : “Ce type a fait un sacré boulot.” Et qu’il ne me coupe pas le robinet.