cd /news/large-language-models/when-the-token-well-runs-dry-a-degra… · home topics large-language-models article
[ARTICLE · art-121440] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=· neutral

When the Token Well Runs Dry: A Degradation State Machine for LLM Services

A developer has detailed a degradation state machine for LLM services that handles quota exhaustion gracefully, transitioning through NORMAL, WARNING, DEGRADED, and EXHAUSTED states to cache responses or return 503s instead of crashing. The implementation, shared as part of MonkeyCode's product outreach, includes a quota tracker, response cache, and request router to prioritize critical requests and serve cached answers when tokens run low.

read4 min views1 publishedSep 4, 2026

The alert fires at 3:14 AM. Your LLM service returns 500s. The free quota is gone. You check the dashboard. 10,000,000 tokens. Zero remaining.

Most guides teach prevention. Budgets. Ledgers. Pre-checks. This one teaches survival. What happens after the quota dies? The answer determines if your users stay.

I built a degradation state machine. It turns quota exhaustion into a designed state. Not a crash. This article shows the full implementation.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Free tiers end. That is a fact. Design for it.

State Trigger Behavior
NORMAL Usage < 70% Full service
WARNING Usage >= 70% Cache responses
DEGRADED Usage >= 90% Critical requests only
EXHAUSTED Usage >= 100% Cache or 503

The thresholds are config. Tune them for your traffic. The state machine is the core.

MonkeyCode's free tier includes 10M tokens and a free server option. Quotas change. Verify the current numbers first.

The tracker polls the usage endpoint. It stores the remaining count in memory.

class QuotaTracker:
    def __init__(self, cap: int):
        self.cap = cap
        self.used = 0

    def record(self, tokens: int):
        self.used += tokens

    def ratio(self) -> float:
        return self.used / self.cap

    def state(self) -> str:
        r = self.ratio()
        if r >= 1.0:
            return "EXHAUSTED"
        if r >= 0.9:
            return "DEGRADED"
        if r >= 0.7:
            return "WARNING"
        return "NORMAL"

Simple arithmetic. Four states. No magic.

The WARNING state needs a cache. Every response gets stored. Keyed by the prompt hash.

import hashlib

class ResponseCache:
    def __init__(self):
        self.store = {}

    def key(self, prompt: str) -> str:
        return hashlib.sha256(prompt.encode()).hexdigest()

    def get(self, prompt: str):
        return self.store.get(self.key(prompt))

    def put(self, prompt: str, response: str):
        self.store[self.key(prompt)] = response

The cache is a dictionary. For production, use Redis. For a free server, a dict is fine.

Not all requests are equal. Some deserve the last tokens. Others can wait.

def classify_request(payload: dict) -> str:
    if payload.get("priority") == "critical":
        return "critical"
    if payload.get("task") == "classify":
        return "cheap"
    return "normal"

Critical requests keep the service alive. Cheap requests can use cached answers. Normal requests wait.

This is the heart. The router checks the state before every call.

def route(payload: dict, tracker: QuotaTracker, cache: ResponseCache):
    state = tracker.state()
    task = classify_request(payload)

    if state == "EXHAUSTED":
        cached = cache.get(payload["text"])
        if cached:
            return {"source": "cache", "label": cached}
        return {"source": "none", "error": "quota exhausted"}, 503

    if state == "DEGRADED" and task != "critical":
        cached = cache.get(payload["text"])
        if cached:
            return {"source": "cache", "label": cached}
        return {"source": "none", "error": "degraded"}, 503

    if state == "WARNING":
        cached = cache.get(payload["text"])
        if cached:
            return {"source": "cache", "label": cached}

    result = call_model(payload["text"])
    cache.put(payload["text"], result)
    tracker.record(estimate_tokens(payload["text"], result))
    return {"source": "live", "label": result}

Read the logic top to bottom. Each state adds a restriction. The service never crashes. It degrades.

A state machine needs a test. Simulate quota exhaustion. Verify each state.

def test_degradation():
    tracker = QuotaTracker(cap=1000)
    cache = ResponseCache()

    cache.put("hello", "greeting")

    assert tracker.state() == "NORMAL"

    tracker.used = 700
    assert tracker.state() == "WARNING"

    tracker.used = 900
    assert tracker.state() == "DEGRADED"

    tracker.used = 1000
    assert tracker.state() == "EXHAUSTED"

    result = route({"text": "hello"}, tracker, cache)
    assert result["source"] == "cache"

    result = route({"text": "unknown"}, tracker, cache)
    assert result[1] == 503

    print("All degradation tests passed")

Run it.

python test_degradation.py

The free server option hosts this. No credit card. No billing alarm.

git clone <your-repo>
cd <your-repo>
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000

Add a health endpoint that reports the current state.

curl http://localhost:8000/state

A simple curl tells you the truth. No dashboard needed.

Caching has limits. Unique prompts miss. Long conversations miss. Time-sensitive answers miss.

The cache hit rate determines your survival time. Measure it.

hits = sum(1 for r in results if r["source"] == "cache")
print(f"Hit rate: {hits / len(results):.0%}")

A 40% hit rate extends the service by days. A 5% hit rate barely helps.

Teams with SLAs need paid capacity. Teams with real-time requirements need dedicated endpoints. Teams with high concurrency need more than a free server.

This pattern is for prototypes. For internal tools. For services where a 503 is acceptable. For developers who want a service that fails gracefully.

Quota exhaustion is a state. Design for it. The state machine turns a crash into a graceful degradation. Cache what you can. Protect what matters. Fail loudly when you must.

If you want to experiment with this pattern, MonkeyCode's free tier is a place to start. Check the current quota first. Then build something that survives the month.

MonkeyCode provides free models that can run this workflow.

── more in #large-language-models 4 stories · sorted by recency
── more on @monkeycode 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/when-the-token-well-…] indexed:0 read:4min 2026-09-04 ·