# One Project Ate the Shared Free Tier: A Per-Project Quota Pattern for LLM Gateways

> Source: <https://dev.to/codepro_9661/one-project-ate-the-shared-free-tier-a-per-project-quota-pattern-for-llm-gateways-27i2>
> Published: 2026-08-24 19:20:45+00:00

Three projects shared one gateway, one API key, and one 10-million-token allowance. On day nineteen, a batch job that summarized support tickets consumed 7.1 million tokens in four hours, and every interactive request from the other two projects started failing with quota errors. The dashboard showed a single number, zero tokens remaining, and it did not say which project had spent them.

The failure was not caused by the batch job alone. The gateway had no concept of a project, so every request drew from the same pool, and the first consumer to exhaust the pool won. This article describes a per-project quota layer that was built while running several small services on MonkeyCode's free model access, which currently includes a 10-million-token allowance, hosted on the free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The quota pattern is plain Python and works with any OpenAI-compatible endpoint.

The gateway authenticated requests with a single API key and had no way to attribute usage to a specific consumer. The first step was to introduce a project identifier, either as a header or a path prefix. Every request carried its project ID, and the quota layer used that ID to look up the project's budget.

```
PROJECTS = {
    "triage": ProjectQuota("triage", daily_token_limit=3_000_000, per_minute_request_limit=60),
    "search": ProjectQuota("search", daily_token_limit=4_000_000, per_minute_request_limit=120),
    "summarize": ProjectQuota("summarize", daily_token_limit=2_000_000, per_minute_request_limit=30),
}
```

The limits were chosen to sum to 9 million tokens, leaving a 10 percent buffer for unexpected traffic. The buffer was the safety margin that the previous single-pool design did not have.

The quota check ran before the upstream request, using an estimate of the token cost. The estimate was derived from the prompt length and the expected completion length, and it was deliberately conservative. A request that would exceed the project's remaining budget was rejected with a 429 response and a clear message.

``` php
def estimate_tokens(prompt: str, max_completion: int) -> int:
    return len(prompt) // 4 + max_completion

def precheck(project_id: str, prompt: str, max_completion: int) -> bool:
    quota = PROJECTS[project_id]
    estimated = estimate_tokens(prompt, max_completion)
    if quota.used_tokens + estimated > quota.daily_token_limit:
        return False
    if quota.window_requests >= quota.per_minute_request_limit:
        return False
    return True
```

The pre-check prevented the common failure mode where a retry loop consumed the entire allowance before any monitoring alert could fire. The batch job that caused the original incident would have been stopped at the first retry, because its project budget was only 2 million tokens.

The estimate was never trusted as the final number. After each upstream response, the gateway read the actual usage from the response and added it to the project's counter. The reconciliation step made the quota accurate even when the estimate was off by a factor of two.

``` python
def reconcile(project_id: str, usage: dict):
    actual = usage.get("total_tokens", 0)
    quota = PROJECTS[project_id]
    quota.used_tokens += actual
    quota.window_requests += 1
```

The combination of pre-check and post-reconciliation meant that a single bad estimate could only cause a small overshoot, never a runaway.

Rejecting a request was the last resort. The gateway used a three-step ladder that gave clients a chance to adapt. At 80 percent of the daily budget, every response included a warning header. At 100 percent, requests were queued for up to thirty seconds. Over the per-minute cap, requests were rejected immediately with a Retry-After header.

``` php
def should_queue(quota: ProjectQuota) -> bool:
    return quota.used_tokens >= quota.daily_token_limit

async def proxy_with_degradation(project_id: str, request):
    quota = PROJECTS[project_id]
    if should_queue(quota):
        try:
            await asyncio.wait_for(queue.put(request), timeout=30)
        except asyncio.TimeoutError:
            return Response("Quota exceeded, try later", status=429)
    return await forward(request)
```

The queue was a simple asyncio.Queue with a timeout. Requests that waited too long were dropped with a clear error, which was better than a silent failure because the client could retry with a backoff.

The in-memory counters were lost on restart, which meant a gateway reboot reset every project to zero and allowed a fresh burst of overspending. A SQLite table stored the current usage per project, and the reset happened lazily on the first request of a new day.

```
CREATE TABLE IF NOT EXISTS quota_usage (
    project_id TEXT PRIMARY KEY,
    date TEXT NOT NULL,
    tokens_used INTEGER NOT NULL
);
```

The lazy reset avoided a cron job and made the table self-maintaining. Old rows accumulated, but a weekly DELETE statement kept the table small.

The quota layer prevents overspending, but it does not make the free tier reliable. A free server has no uptime guarantee, and the allowance itself can change without notice. The pattern also assumes that all projects have equal priority. If one project is genuinely more important than the others, the gateway needs a priority queue, not just a quota check.

The SQLite storage is a single point of failure. A gateway that runs multiple replicas needs a shared store such as Redis, and the quota check becomes a distributed coordination problem. The pattern described here is for a single-instance gateway, which is the right scale for a side project.

A team running production workloads with contractual token budgets should use a proper rate limiter and quota system with distributed coordination. Anyone who needs per-request audit trails for billing should keep the raw usage logs and build a separate attribution pipeline. The pattern here is for solo developers and small teams who want to share one free allowance across several projects without one project ruining the others.

The quota layer is about thirty lines of Python, and it turns a shared free tier from a liability into a predictable resource. The same pattern works with any OpenAI-compatible endpoint, and the SQLite table can be swapped for Redis when the gateway grows. For those who want to try it, MonkeyCode's open-source project includes a gateway that can host this quota layer, and the free server option provides a stable place to run it. The repository has the configuration details, and the pattern is short enough to adapt in an afternoon.
