Un filtro de Kalman para no molestar al servidor (o el placer culpable de la sobreingeniería)

| Última modificación: 4 de mayo de 2026 | Tiempo de Lectura: 7 minutos
Premios Blog KeepCoding 2025

Co-Fundador de KeepCoding

Tengo una app de menú bar que necesita saber un número. Un porcentaje de 0 a 100. Para obtenerlo, llama a un servidor cada 30 segundos.

Haz las cuentas: 30 segundos son 2 llamadas por minuto, 120 por hora, 960 en una jornada de 8 horas. Casi mil peticiones HTTP al día para leer un número que a veces no cambia en 20 minutos.

Eso no es monitorización. Es acoso al servidor.

El problema de verdad no es técnico. Es político.

Cuando dependes de un API que no controlas — que no es público, que no tiene rate limits documentados, que pertenece a una empresa que puede cambiar sus Terms of Service cualquier martes — cada petición innecesaria es un riesgo. No de timeout. De que te corten el grifo.

El endpoint que uso no está documentado. Funciona hoy. Lleva meses funcionando. Pero cada petición que envío es una línea más en un log que alguien en Anthropic puede mirar y decidir que una app de terceros está haciendo demasiado ruido.

Así que la pregunta no es «¿cómo hago polling más rápido?» sino «¿cómo hago polling lo menos posible sin perder información?»

Y ahí, amigo, es donde un ingeniero razonable haría un if y un ingeniero con un guilty pleasure por la sobreingeniería monta un filtro de Kalman.

La solución ingenua (y por qué falla)

El primer instinto es sencillo: si el número no ha cambiado, no preguntes.

si el_valor == el_valor_anterior:
    espera más
si no:
    vuelve a 30 segundos

Funciona fatal. El valor cambia cuando haces algo (envías mensajes, usas tokens). Pero también cambia cuando no haces nada — la cuota tiene una ventana deslizante de 5 horas, así que los tokens viejos caducan solos. Y si estás usando el servicio desde otro dispositivo, el valor sube sin que tú lo sepas.

No basta con mirar si cambió. Necesitas predecir cuándo va a cambiar y con qué confianza.

Kalman para gente con prisa

Un filtro de Kalman es una máquina de combinar dos fuentes de información imperfectas.

Imagina que estás en una habitación sin ventanas y quieres saber la temperatura de fuera. Tienes dos opciones:

  1. Tu modelo mental: «Son las 3 de la tarde en marzo en Madrid, así que calculo unos 18°C». Es una estimación razonable, pero no perfecta — puede haber llovido, puede haber viento.
  2. Un termómetro ruidoso: puedes asomarte al balcón, pero tu termómetro es barato y fluctúa ±3°C.

Ninguna fuente es perfecta. El filtro de Kalman dice: combina ambas, pero dale más peso a la que sea más fiable en cada momento.

Si acabas de mirar el termómetro hace 10 segundos, tu modelo mental es muy bueno — confía en él y no te molestes en volver a mirar. Si llevas una hora sin mirar, tu modelo se ha degradado — sal al balcón.

La clave es la varianza: un número que mide «cuánto me fío de mi estimación actual». Empieza en cero justo después de mirar el termómetro, y crece con el tiempo. Cuando cruza un umbral, el filtro dice «ya no me fío, necesito un dato real.»

En mi caso:

  • El modelo mental = coste local de tokens. Sé lo que he consumido en Claude Code, así que puedo calcular cuánto debería haber subido la cuota.
  • El termómetro = la API de Anthropic. Dato real, pero cada lectura tiene un coste político y energético.
  • La varianza = incertidumbre que crece con el tiempo. Si he usado el servicio desde el navegador o el móvil, mi modelo local no lo sabe — y eso degrada la predicción.

Un Kalman completo (multidimensional, con matrices de covarianza) sería matar moscas a cañonazos. Lo mío es escalar: un solo estado (utilización), un solo sensor (la API), un modelo lineal (coste/presupuesto). 20 líneas de código. La versión mínima que resuelve el problema.

Traducido a mi problema concreto:

  • Predicción: utilización_estimada = último_dato_real + (coste_local_nuevo / presupuesto) × 100
  • Corrección: cada vez que el servidor responde, el filtro resetea su varianza a cero.
  • Incertidumbre: la varianza crece linealmente con el tiempo. σ = √(Q × segundos_desde_última_corrección).

El truco: el filtro decide cuándo preguntar

Aquí es donde la sobreingeniería se justifica. El filtro no solo estima el valor — decide cuándo necesita un dato real. Cinco reglas, evaluadas en cada tick:

Regla Disparador Por qué
Ventana reseteada now ≥ resetsAt Los tokens caducaron. El dato anterior no vale.
Incertidumbre alta σ > 5% Me fío poco de mi predicción.
Cruce de frontera El intervalo de confianza cruza el 80%, 95% o 100% Estoy cerca de un cambio de zona. El usuario necesita saberlo.
Proximidad utilización a menos de 8% de una frontera Puedo estar al otro lado y no saberlo (actividad externa).
Timeout de seguridad 15 minutos sin dato real Por si acaso. La paranoia es una virtud en software de monitorización.

Si ninguna regla se dispara, el filtro dice «tranquilo, yo controlo» y la app no hace la petición HTTP. El valor que muestra al usuario es la estimación local.

Ojo al dato: la estimación local cuesta cero red, cero batería, cero riesgo. Es aritmética pura en memoria.

Los números: antes y después

Una jornada típica con la cuota estable (uso moderado, sin picos):

Escenario Peticiones/hora Peticiones/día (8h)
Polling fijo 30s 120 960
Con estimador bayesiano 15-30 120-240
Estimador + dormant 4-10 30-80

Eso es una reducción del 75-97% en llamadas a red. No está mal para «solo» hacer predicciones locales entre peticiones reales.

Pero espera, hay más (la degradación adaptativa)

El filtro de Kalman resuelve el problema de «cuándo preguntar». Pero hay otro nivel: cuánto esfuerzo invertir en preguntar.

La app tiene una política de polling que ajusta el intervalo base según el contexto:

Actividad reciente (< 10 min)  → 30s
Idle moderado (10 min - 1h)    → 120s
Idle largo (> 1h)              → 300s
Cuota > 80%                    → 30s siempre (zona crítica)
Modo ahorro de energía         → 2× el intervalo base
Errores consecutivos           → backoff exponencial (hasta 5 min)

Cada nivel es una decisión de «cuánta información necesito ahora mismo«. Si no estás programando, ¿para qué gastar batería comprobando tu cuota cada 30 segundos? Si tu portátil está al 15% de batería, ¿merece la pena hacer el doble de peticiones HTTP?

El modo dormant: cuando la app se duerme sola

Y aquí llega mi parte favorita. La que admito que quizá no hacía falta, pero que me dejó con la sonrisa de quien ha hecho algo innecesariamente elegante.

Cuando el estimador bayesiano produce cinco estimaciones consecutivas donde el valor cambia menos de 0.5%, la app entra en modo dormant:

  1. Para el timer.
  2. Deja de estimar.
  3. Se queda escuchando el filesystem.

¿Por qué el filesystem? Porque si estás usando el servicio, se generan ficheros locales. Cuando el watcher de ficheros detecta actividad, la app despierta, hace un API call inmediato para anclarse a la realidad, y vuelve al ciclo normal.

Es como un perro dormido junto a la puerta. No gasta energía, pero si oye la llave, está despierto al instante.

El resultado: si dejas de trabajar a las 14:00 y vuelves a las 16:00, la app ha hecho cero peticiones durante esas dos horas. Cero. Ni polling a 5 minutos, ni keepalive, ni heartbeat. El timer literalmente no existe. Y cuando vuelves, en milisegundos tienes el dato actualizado.

«¿Y no era más fácil hacer un setInterval de 5 minutos y ya?»

Sí. Mucho más fácil. Y probablemente suficiente para el 90% de los usuarios.

Pero hay una diferencia que importa cuando tu app corre 8 horas al día en segundo plano:

setInterval(5min) Estimador + dormant
Peticiones/día idle 96 0
Peticiones/día activo 96 30-80 (adaptativo)
Latencia de actualización 0-5 min < 1s (FSEvent wake)
Consumo batería idle Constante Cero
Precisión zona crítica Igual (5 min delay) 30s (zona > 80%)

La fila que importa es la tercera. Con un timer fijo de 5 minutos, si la cuota salta del 78% al 95% entre dos ticks, el usuario no se entera hasta que pasan hasta 5 minutos. Con el estimador bayesiano, el intervalo baja a 10 segundos cuando estima localmente, y el filtro pide un dato real al servidor en cuanto su intervalo de confianza cruza el 80%.

Dicho en cristiano: reacciona más rápido haciendo menos peticiones.

La parte seria: por qué esto es software responsable

Voy a quitarme el sombrero de over-engineer satisfecho y ponerme el de ingeniero a secas.

Cada petición HTTP que tu app hace en segundo plano tiene un coste que pagas tú, que paga el servidor y que paga el planeta. No es retórica. Es termodinámica. Un wakeup de red en un portátil dormido enciende la radio WiFi, negocia TLS, espera respuesta, procesa datos y vuelve a dormir. Multiplicado por mil apps haciendo lo mismo, se convierte en la razón por la que tu MacBook aguanta 6 horas en vez de 10.

Apple lo sabe. Por eso macOS tiene App Nap, Timer Coalescing y castiga a las apps con Energy Impact alto. Mi punto de partida era una app con 857 de Energy Impact. El objetivo era bajarla a menos de 5.

El estimador bayesiano con dormant no fue un capricho. Fue la única forma de conseguir ese número sin sacrificar la experiencia del usuario. Hacer menos peticiones era obligatorio. Hacerlas de forma inteligente era el reto.

La receta, por si te sirve

Si tienes una app que hace polling a un servidor y quieres reducir las peticiones sin perder reactividad:

  1. Mide si puedes predecir localmente. Si el valor que lees depende de datos que también tienes en local, puedes interpolar entre llamadas al servidor.

  2. Modela la incertidumbre. No basta con predecir. Necesitas saber cuánto confías en la predicción. Un filtro de Kalman escalar son 20 líneas de código.

  3. Define fronteras de decisión. ¿En qué rangos del valor importa la precisión? No desperdicies precisión en zonas donde al usuario le da igual (0-60%), y concentra las mediciones donde importa (80-100%).

  4. Adapta al contexto. Batería baja, idle largo, errores del servidor — cada contexto tiene un coste distinto de hacer una petición. Tu polling debería reflejarlo.

  5. Ten un modo cero. Si no hay actividad, no hagas nada. Literalmente nada. No un timer largo. Nada. Un evento de filesystem o de red te despertará cuando haga falta.

El guilty pleasure

Voy a ser honesto: ¿hacía falta un filtro de Kalman para una app de menú bar que muestra un porcentaje? Probablemente no. Un par de if con heurísticas habrían cubierto el 80% del problema.

Pero el 20% restante es la diferencia entre una app que «más o menos funciona» y una que un usuario puede dejar corriendo 12 horas sin notar que está ahí. Entre 857 de Energy Impact y menos de 5. Entre 960 peticiones al día y 30.

A veces el placer culpable de la sobreingeniería es exactamente lo que el problema necesitaba. Solo que no lo sabías hasta que lo montaste.

Y si alguien en Anthropic mira algún día los logs de su servidor y ve que mi app hace 30 peticiones al día en vez de mil, espero que piense: «Este tío se lo ha currado». Y no me corte el grifo.


Read this article in English.

Noticias recientes del mundo tech


¡CONVOCATORIA ABIERTA!

Desarrollo de apps móviles ios & Android

Full Stack Bootcamp

Clases en Directo | Profesores en Activo | Temario 100% actualizado

Descárgate también el informe de tendencias en el mercado laboral 2026.

Fórmate con planes adaptados a tus objetivos y logra resultados en tiempo récord.
KeepCoding Bootcamps
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.