DEV Community

Joe Lin for BeGoodTool.com

Posted on

How a Caffeine Tracker Estimates What Is Still in Your System

A caffeine log is easy to misunderstand if it only shows today's total. Two drinks consumed hours apart do not contribute equally to what remains now. I built this tracker as a manual log plus a decay curve, not as an always-on monitor, so the calculation stays explicit about what it knows. The useful implementation lesson is to keep dose, time, half-life, and display thresholds separate instead of hiding everything in a single “daily total.”

Each drink contributes an exponentially decaying dose

The page includes presets for coffee, espresso, black tea, matcha, energy drinks, cola, and a custom dose. For every stored record, the residual calculation applies a half-life:

const halfLife = Math.max(0.1,
  Number(halfLifeHours.value) || 5) * MS_PER_HOUR;

return records.value.reduce((sum, record) => {
  if (ts < record.intakeTime) return sum;
  return sum + record.mg * Math.pow(
    0.5, (ts - record.intakeTime) / halfLife
  );
}, 0);
Enter fullscreen mode Exit fullscreen mode

The equation says that after one half-life, half a recorded dose remains; after two, one quarter remains. A 100 mg drink with the default five-hour half-life contributes about 50 mg five hours later and 25 mg ten hours later. If another 95 mg coffee was consumed in between, the current estimate is the sum of both residuals, not the decay of whichever drink was latest.

The default half-life is five hours, but it is a user-editable assumption. Math.max(0.1, ...) prevents a zero denominator, and future records are ignored when the chart asks for a timestamp before intake. The code stores times as millisecond timestamps, while the form parses a datetime-local value into a timestamp; that keeps the decay formula independent from how a browser formats dates.

What “today” means in this UI

The tracker reports a daily intake total based on the local date key of each recorded timestamp. That is different from residual caffeine, which can include late-night drinks from the previous local date. Records are retained while their age is under 48 hours, so caffeine from last night is not dropped at midnight just because the calendar label changed.

This distinction prevents a common misleading display. Someone can have a low “today” total and still have substantial residual caffeine from yesterday evening. Conversely, a large morning dose can count toward today's total while decaying through the afternoon. The page is not reading a sensor; it is adding only the records the user entered.

The 24-hour chart is a projection of known records

The component refreshes nowTick every minute and builds Chart.js data from the same residualAt function used for the current estimate. It samples the next 24 hours at 30-minute intervals:

function chartLabelsAndData() {
  const labels = [];
  const data = [];
  const start = nowTick.value;
  for (let i = 0; i <= 48; i++) {
    const ts = start + i * 30 * 60 * 1000;
    labels.push(formatDateTime(ts));
    data.push(Math.round(residualAt(ts) * 10) / 10);
  }
  return { labels, data };
}
Enter fullscreen mode Exit fullscreen mode

There are 49 points including both endpoints, which covers 24 hours without implying minute-level precision. Changing the half-life or adding a drink changes the curve because there is no second projection formula to keep in sync. The chart's second dataset repeats the configured sleep threshold across every sample.

That bedtime line defaults to 50 mg and is a reference threshold. The daily status uses a separate 400 mg adult reference. Keeping 50 and 400 distinct matters: one is a user-facing planning line for bedtime, while the other is a broad daily reference, not a personalized medical safety boundary. A chart crossing 50 does not diagnose insomnia or prove that sleep will be affected.

For a concrete walkthrough, log a 95 mg coffee at 8:00 and a 63 mg espresso at 13:00 with the five-hour half-life. At 18:00 the coffee contributes roughly 47.5 mg and the espresso roughly 53.6 mg, so the total is about 101 mg before rounding. The chart then projects both curves forward and lets you see when their sum falls below the selected reference. It is the additive shape, not a claim that the body follows a perfect single-compartment model, that makes multiple entries easier to reason about.

The graph is intentionally sampled rather than continuously calculated for display. There are 49 points including the current time and the 24-hour endpoint, one every 30 minutes. That is enough resolution for a planning curve and keeps the chart readable on a phone. The underlying residualAt function still accepts arbitrary timestamps, so the current number and each chart point use the same decay logic.

Manual data is a real edge case

Records, half-life, and threshold are saved under begoodtool_caffeineTrackerCalculator_v1 in localStorage. Reloading can restore them, but closing the page stops the minute refresh and a forgotten drink never reaches the curve. The 48-hour filter also means old records are deliberately removed from the active model rather than being treated as permanent history.

Preset caffeine values are approximate and serving sizes vary. Half-life varies with metabolism, medication, age, pregnancy, liver function, and individual sensitivity. The component explicitly warns that this is general information, not medical advice; people with health conditions or special circumstances should use qualified local guidance. I turned this manual model into a small free tool: Real-Time Caffeine Buildup Calculator.

Top comments (0)