DEV Community

Riley Lin
Riley Lin

Posted on

Token Forensics: Finding the Process That Ate Your Free Allowance

Three in the morning is when a free tier starts lying to you. Your dashboard shows two hundred requests today, yet the token counter says forty percent of your allowance is gone, and the two numbers refuse to reconcile. You check the model's pricing page, then your code, then your logs, and each one points at a different suspect, which means none of them is the real culprit.

The Suspect List

The first suspect is always the model itself, because a free model with a small context window feels like the obvious explanation for unexpected consumption. You swap the model for a cheaper one and wait a day, and the burn rate does not change at all. The second suspect is your own code, so you read every call site and find nothing obviously wrong, because the problem is not in any single call but in how calls accumulate over time.

The Real Culprit

The real culprit is a cron job that summarizes email threads every five minutes. Each run loads the full thread history from the database and sends it to the model, and one particular thread has grown to four hundred messages over three weeks. The job is not doing more work than before; it is doing the same work on a steadily larger input, and the token cost grows linearly with the thread length while your request count stays flat.

This is the failure mode that monitoring catches and code review misses, because both the code and the request count look healthy. You need a burn-rate breakdown that tracks tokens per process per hour, not just total usage at the end of the day, and the fix is embarrassingly simple once you see the numbers.

# token_forensics.py
import time
from collections import deque
from pathlib import Path

class TokenForensics:
    """Track token burn rate per process and flag anomalies."""

    def __init__(self, log_dir: Path, window_minutes: int = 60):
        self.log_dir = log_dir
        self.window = window_minutes * 60
        self.events = deque()

    def record(self, process: str, tokens: int):
        self.events.append({
            "ts": time.time(),
            "process": process,
            "tokens": tokens,
        })
        cutoff = time.time() - self.window
        while self.events and self.events[0]["ts"] < cutoff:
            self.events.popleft()

    def burn_rate_by_process(self):
        rates = {}
        for e in self.events:
            rates[e["process"]] = rates.get(e["process"], 0) + e["tokens"]
        return {k: v * 3600 / self.window for k, v in rates.items()}

    def detect_anomaly(self, threshold: int):
        return {k: v for k, v in self.burn_rate_by_process().items() if v > threshold}
Enter fullscreen mode Exit fullscreen mode

The script records every token-consuming event as it happens, keeps a sliding one-hour window in memory, and reports the burn rate per process in tokens per hour. You run it as a sidecar next to your agent, and you point it at the same log file that your cron jobs already write, so the instrumentation cost is one line per call site.

# instrument your call site
forensics = TokenForensics(Path("/var/log/agent"))
forensics.record("mail-summarizer", response.usage.total_tokens)
forensics.record("duplicate-sweeper", response.usage.total_tokens)
Enter fullscreen mode Exit fullscreen mode

The first time you run this, the output is boring, and boring is good, because it gives you a baseline. Then you wait for the thread to grow, and after a week the numbers start to diverge: mail-summarizer is burning thirty thousand tokens per hour while every other process sits below four thousand. That is the moment when the fix becomes obvious, and the fix is not a bigger model or a better prompt; it is incremental context.

# Before: send the full thread every time
def summarize_thread(thread_id: str):
    history = get_full_thread(thread_id)  # grows without bound
    return model.summarize(history)

# After: cache the last summary, send only new messages
def summarize_thread_incremental(thread_id: str):
    last = cache.get(thread_id)
    if last is None:
        summary = model.summarize(get_full_thread(thread_id))
        cache.set(thread_id, summary)
        return summary
    new_messages = get_messages_since(thread_id, last.timestamp)
    updated = model.summarize(last.text + new_messages)
    cache.set(thread_id, updated)
    return updated
Enter fullscreen mode Exit fullscreen mode

The incremental version sends the previous summary plus only the new messages, which caps the input size at a constant even when the thread grows forever. The burn rate drops from thirty thousand tokens per hour to about two thousand, and the request count does not change at all, because the same job is running on the same schedule.

Limits and Non-Users

The reusable lesson here is that token consumption is a rate, not a total, and you need to measure it per process before you can reason about it. A total counter tells you that something is wrong; a burn-rate breakdown tells you which process is wrong, and that distinction saves you a full day of hunting. You can reproduce this whole investigation on MonkeyCode's free server with a free model, because the free allowance gives you room to run a monitoring experiment without watching a bill climb. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project, and its current free tier includes a 10-million-token allowance and a free server option; free offers change, so check the repository for the terms that apply today.

The approach has limits, and you should know them before you copy the script. The sliding window keeps everything in memory, so a process that emits millions of events per hour will overflow the deque, and you would need to switch to a time-series database instead. The script also assumes a single server, because it reads a local log file; if your agents run on multiple machines, you need to ship the events to a central collector.

You should not use this script if your token usage is already flat and predictable, because the instrumentation adds a tiny overhead without any insight. Keep the anomaly threshold conservative at first, since a burst of legitimate work will trigger a false alarm and train you to ignore the alerts. And if your agent runs on a distributed fleet, skip the deque entirely and use a proper metrics pipeline, because local forensics cannot see a pattern that spans machines.

The cheapest lesson from this debugging session is that a free tier is the perfect place to learn token accounting, because the cost of a mistake is a reset, not a bill. If you want to watch your own processes burn through an allowance in real time, MonkeyCode's free tier is a reasonable sandbox; just bring your own instrumentation, because the dashboard is the part that actually saves you.

Top comments (0)