I have a menu bar app that needs to know a number. A percentage from 0 to 100. To get it, it calls a server every 30 seconds.

Do the math: 30 seconds means 2 calls per minute, 120 per hour, 960 in an 8-hour workday. Almost a thousand HTTP requests per day to read a number that sometimes doesn’t change for 20 minutes.

That’s not monitoring. That’s harassment.

The real problem isn’t technical. It’s political.

When you depend on an API you don’t control — that’s not public, that has no documented rate limits, that belongs to a company that can change their Terms of Service any given Tuesday — every unnecessary request is a risk. Not of timeout. Of getting cut off.

The endpoint I use isn’t documented. It works today. It’s been working for months. But every request I send is another line in a log that someone at Anthropic might look at and decide a third-party app is making too much noise.

So the question isn’t “how do I poll faster?” but “how do I poll as little as possible without losing information?”

And that’s where a reasonable engineer would write an if statement and an engineer with a guilty pleasure for over-engineering builds a Kalman filter.

The naive solution (and why it fails)

The first instinct is simple: if the number hasn’t changed, don’t ask.

if current_value == previous_value:
    wait longer
else:
    back to 30 seconds

It works terribly. The value changes when you do something (send messages, use tokens). But it also changes when you don’t do anything — the quota has a 5-hour sliding window, so old tokens expire on their own. And if you’re using the service from another device, the value goes up without you knowing.

It’s not enough to check if it changed. You need to predict when it’s going to change and with what confidence.

Kalman for people in a hurry

A Kalman filter is a machine for combining two imperfect sources of information.

Imagine you’re in a windowless room and want to know the temperature outside. You have two options:

  1. Your mental model: “It’s 3 PM in March in Madrid, so I estimate around 64°F”. It’s a reasonable estimate, but not perfect — it might have rained, there might be wind.
  2. A noisy thermometer: you can step out on the balcony, but your thermometer is cheap and fluctuates ±5°F.

Neither source is perfect. The Kalman filter says: combine both, but give more weight to whichever is more reliable at each moment.

If you just looked at the thermometer 10 seconds ago, your mental model is very good — trust it and don’t bother looking again. If you haven’t looked for an hour, your model has degraded — go out to the balcony.

The key is variance: a number that measures “how much I trust my current estimate”. It starts at zero right after looking at the thermometer, and grows with time. When it crosses a threshold, the filter says “I don’t trust this anymore, I need real data.”

In my case:

  • The mental model = local token cost. I know what I’ve consumed in Claude Code, so I can calculate how much the quota should have increased.
  • The thermometer = Anthropic’s API. Real data, but each reading has a political and energy cost.
  • The variance = uncertainty that grows with time. If I’ve used the service from the browser or mobile, my local model doesn’t know — and that degrades the prediction.

A full Kalman filter (multidimensional, with covariance matrices) would be overkill. Mine is scalar: one state (utilization), one sensor (the API), one linear model (cost/budget). 20 lines of code. The minimal version that solves the problem.

Translated to my specific problem:

  • Prediction: estimated_utilization = last_real_data + (new_local_cost / budget) × 100
  • Correction: every time the server responds, the filter resets its variance to zero.
  • Uncertainty: variance grows linearly with time. σ = √(Q × seconds_since_last_correction).

The trick: the filter decides when to ask

This is where over-engineering justifies itself. The filter doesn’t just estimate the value — it decides when it needs real data. Five rules, evaluated on each tick:

RuleTriggerWhy
Window resetnow ≥ resetsAtTokens expired. Previous data is invalid.
High uncertaintyσ > 5%I don’t trust my prediction.
Boundary crossingConfidence interval crosses 80%, 95%, or 100%I’m close to a zone change. User needs to know.
Proximityutilization within 8% of a boundaryI might be on the other side and not know it (external activity).
Safety timeout15 minutes without real dataJust in case. Paranoia is a virtue in monitoring software.

If no rule triggers, the filter says “relax, I got this” and the app doesn’t make the HTTP request. The value it shows the user is the local estimate.

The local estimate costs zero network, zero battery, zero risk. It’s pure arithmetic in memory.

The numbers: before and after

A typical workday with stable quota (moderate use, no spikes):

ScenarioRequests/hourRequests/day (8h)
Fixed 30s polling120960
With Bayesian estimator15-30120-240
Estimator + dormant4-1030-80

That’s a 75-97% reduction in network calls. Not bad for “just” making local predictions between real requests.

But wait, there’s more (adaptive degradation)

The Kalman filter solves the problem of “when to ask”. But there’s another layer: how much effort to invest in asking.

The app has a polling policy that adjusts the base interval based on context:

Recent activity (< 10 min)    → 30s
Moderate idle (10 min - 1h)   → 120s
Long idle (> 1h)              → 300s
Quota > 80%                   → 30s always (critical zone)
Power saving mode             → 2× base interval
Consecutive errors            → exponential backoff (up to 5 min)

Each level is a decision of “how much information do I need right now”. If you’re not coding, why spend battery checking your quota every 30 seconds? If your laptop is at 15% battery, is it worth making twice as many HTTP requests?

Dormant mode: when the app sleeps itself

And here comes my favorite part. The one I admit might not have been necessary, but left me with the smile of someone who’s done something unnecessarily elegant.

When the Bayesian estimator produces five consecutive estimates where the value changes less than 0.5%, the app enters dormant mode:

  1. Stops the timer.
  2. Stops estimating.
  3. Starts listening to the filesystem.

Why the filesystem? Because if you’re using the service, local files get generated. When the file watcher detects activity, the app wakes up, makes an immediate API call to anchor itself to reality, and returns to the normal cycle.

It’s like a dog sleeping by the door. It doesn’t waste energy, but if it hears the key, it’s awake instantly.

The result: if you stop working at 2:00 PM and return at 4:00 PM, the app has made zero requests during those two hours. Zero. No 5-minute polling, no keepalive, no heartbeat. The timer literally doesn’t exist. And when you come back, you have updated data in milliseconds.

“Wouldn’t it have been easier to just do a 5-minute setInterval and call it a day?”

Yes. Much easier. And probably sufficient for 90% of users.

But there’s a difference that matters when your app runs 8 hours a day in the background:

setInterval(5min)Estimator + dormant
Requests/day idle960
Requests/day active9630-80 (adaptive)
Update latency0-5 min< 1s (FSEvent wake)
Battery consumption idleConstantZero
Critical zone precisionSame (5 min delay)30s (zone > 80%)

The third row is what matters. With a fixed 5-minute timer, if quota jumps from 78% to 95% between two ticks, the user doesn’t find out for up to 5 minutes. With the Bayesian estimator, the interval drops to 10 seconds when estimating locally, and the filter requests real data from the server as soon as its confidence interval crosses 80%.

In other words: it reacts faster by making fewer requests.

The serious part: why this is responsible software

I’m going to take off my satisfied over-engineer hat and put on my plain engineer hat.

Every HTTP request your app makes in the background has a cost that you pay, the server pays, and the planet pays. This isn’t rhetoric. It’s thermodynamics. A network wakeup on a sleeping laptop turns on the WiFi radio, negotiates TLS, waits for response, processes data, and goes back to sleep. Multiplied by a thousand apps doing the same thing, it becomes the reason your MacBook lasts 6 hours instead of 10.

Apple knows this. That’s why macOS has App Nap, Timer Coalescing, and penalizes apps with high Energy Impact. My starting point was an app with 857 Energy Impact. The goal was to get it below 5.

The Bayesian estimator with dormant mode wasn’t a whim. It was the only way to hit that number without sacrificing user experience. Making fewer requests was mandatory. Making them intelligently was the challenge.

The recipe, in case it helps you

If you have an app that polls a server and want to reduce requests without losing responsiveness:

  1. Measure if you can predict locally. If the value you’re reading depends on data you also have locally, you can interpolate between server calls.

  2. Model uncertainty. It’s not enough to predict. You need to know how much you trust the prediction. A scalar Kalman filter is 20 lines of code.

  3. Define decision boundaries. In what ranges of the value does precision matter? Don’t waste precision in zones where the user doesn’t care (0-60%), and concentrate measurements where it matters (80-100%).

  4. Adapt to context. Low battery, long idle, server errors — each context has a different cost for making a request. Your polling should reflect that.

  5. Have a zero mode. If there’s no activity, do nothing. Literally nothing. Not a long timer. Nothing. A filesystem or network event will wake you up when needed.

The guilty pleasure

I’ll be honest: did a menu bar app that shows a percentage need a Kalman filter? Probably not. A couple of if statements with heuristics would have covered 80% of the problem.

But that remaining 20% is the difference between an app that “sort of works” and one that a user can leave running 12 hours without noticing it’s there. Between 857 Energy Impact and less than 5. Between 960 requests per day and 30.

Sometimes the guilty pleasure of over-engineering is exactly what the problem needed. You just don’t know it until you build it.

And if someone at Anthropic ever looks at their server logs and sees that my app makes 30 requests per day instead of a thousand, I hope they think: “This guy really put in the work”. And don’t cut off my access.