I stress-tested Groq’s free tier, Gemini Flash 2.0’s free tier, and an open-router free model under realistic request patterns to find out exactly where each breaks. The rate limits are real, the failure modes are specific, and the recovery behavior varies enough to matter for architecture decisions.
Analysis Briefing
- Topic: Free-tier LLM API stress test under realistic production load
- Analyst: Mike D (@MrComputerScience)
- Context: Stress-tested in dialogue with Gemini 2.0 Flash
- Source: Pithy Cyborg | Pithy Security
- Key Question: At what exact request rate does each free tier collapse, and what happens when it does?
The Test Setup
All tests ran from a single Python process using asyncio with concurrent request pools of 1, 5, 10, 20, and 50 simultaneous requests. Each request sent a 200-token prompt and requested a 300-token completion, representing a realistic support or summarization task.
Tests ran for ten minutes at each concurrency level. I measured throughput, error rate, time to first rate limit error, and recovery behavior after hitting the limit. No retries during the test. Raw behavior only.
import asyncio
import time
from groq import AsyncGroq
async def send_request(client, semaphore, results):
async with semaphore:
start = time.monotonic()
try:
response = await client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": TEST_PROMPT}],
max_tokens=300,
)
results.append({"status": "success", "latency": time.monotonic() - start})
except Exception as e:
results.append({"status": "error", "error": str(e),
"latency": time.monotonic() - start})
async def run_load_test(concurrency: int, duration_seconds: int):
client = AsyncGroq()
semaphore = asyncio.Semaphore(concurrency)
results = []
end_time = time.monotonic() + duration_seconds
tasks = []
while time.monotonic() < end_time:
tasks.append(asyncio.create_task(
send_request(client, semaphore, results)
))
await asyncio.sleep(0.1) # 10 req/sec attempt rate
await asyncio.gather(*tasks, return_exceptions=True)
return results
Groq Free Tier Results
At concurrency 1: Groq handled sustained load cleanly. Median latency was 380ms for a 300-token completion, which is fast. No rate limit errors over ten minutes.
At concurrency 5: Throughput scaled linearly. Latency stayed under 600ms median. First rate limit error appeared at minute 4. The error is a clean 429 with a retry-after header specifying seconds until reset. Recovery after the rate limit window reset was immediate.
At concurrency 10: Rate limit errors appeared within 90 seconds. The 429 responses came in bursts: clean traffic for 45-60 seconds, then a burst of 429s as the token-per-minute limit hit, then clean traffic again as the window reset. The burst pattern means a naive retry-on-429 loop would produce thundering herd behavior at window reset.
At concurrency 20 and 50: Majority of requests returned 429 within the first minute. Groq’s free tier limit is 6,000 tokens per minute on Llama 3.3 70B. At 500 tokens per request and 20 concurrent requests, you exhaust the limit in about 36 seconds of sustained traffic.
Failure mode: Clean 429 with retry-after. The limit is per-minute, not per-day, so recovery is fast. Groq is appropriate for bursty workloads with low sustained concurrency, not for steady high-concurrency traffic.
Gemini Flash 2.0 Free Tier Results
At concurrency 1-5: Clean. Gemini Flash’s free tier is 1,500 requests per day and 15 requests per minute. At concurrency 5 with 10 attempts per second, the per-minute limit was the binding constraint. First rate limit error appeared within 8 seconds at concurrency 5.
Rate limit behavior: Gemini’s 429 response does not include a retry-after header consistently. The retry delay must be inferred or hard-coded. A 60-second backoff after a 429 is the safe default for the per-minute limit. The per-day limit (1,500 requests) is easy to exhaust in testing if you are not tracking it.
Latency: Higher than Groq at low concurrency. Median 800ms to first token for a 300-token completion. Still acceptable for non-interactive workloads.
Failure mode: 429 without reliable retry guidance. The per-minute limit is tight at 15 requests per minute. Gemini Flash free tier is appropriate for low-volume workloads where latency tolerance is high.
OpenRouter Free Models
OpenRouter provides access to several open models on a free tier, including Llama 3.3 70B and Mistral 7B, subject to rate limits that vary by model and are not always clearly documented.
Behavior under load: Less predictable than Groq or Gemini. Rate limit responses appeared at lower concurrency than documented limits suggested, and the retry-after values in 429 responses were sometimes inconsistent with actual recovery times.
Latency: Variable. The same model through OpenRouter had 2-4x higher median latency than through Groq, reflecting OpenRouter’s routing overhead and shared infrastructure.
Use case: OpenRouter free tier is appropriate for experimentation and model comparison, not for sustained production traffic even at low concurrency. The latency variance alone makes it unsuitable for user-facing applications.
The Architecture That Survives Free-Tier Limits
No single free tier handles sustained production traffic. The architecture that works combines multiple free tiers with token-aware routing:
class TokenAwareRouter:
def __init__(self):
self.providers = {
"groq": {"tpm_limit": 6000, "tokens_used": 0, "window_start": time.time()},
"gemini": {"rpm_limit": 15, "requests_this_minute": 0, "minute_start": time.time()},
}
def select_provider(self, estimated_tokens: int) -> str:
now = time.time()
# Reset Groq window if minute has passed
groq = self.providers["groq"]
if now - groq["window_start"] > 60:
groq["tokens_used"] = 0
groq["window_start"] = now
# Reset Gemini window if minute has passed
gemini = self.providers["gemini"]
if now - gemini["minute_start"] > 60:
gemini["requests_this_minute"] = 0
gemini["minute_start"] = now
# Route to provider with headroom
if groq["tokens_used"] + estimated_tokens < groq["tpm_limit"] * 0.8:
groq["tokens_used"] += estimated_tokens
return "groq"
if gemini["requests_this_minute"] < gemini["rpm_limit"] * 0.8:
gemini["requests_this_minute"] += 1
return "gemini"
return None # All providers at capacity, caller handles queuing
The 0.8 multiplier leaves 20% headroom before the hard limit. Hitting 80% of the limit before routing away prevents the burst of 429s that appears when you drive a provider exactly to its limit.
What This Means For You
- Track token consumption per provider per window actively, not reactively. Routing based on headroom avoids the thundering herd that appears when you hit the limit and retry simultaneously from multiple concurrent requests.
- Treat Groq as your speed tier and Gemini as your backup. Groq is fastest and has the cleanest rate limit signaling. Gemini has a per-day limit that is harder to exhaust for typical workloads.
- Never rely on a single free tier for user-facing traffic. The per-minute limits are designed for development, not production. A multi-provider router with headroom tracking is the minimum viable production architecture at zero cost.
- Build your load tests before you build your application. The failure modes only appear under concurrency. Run your test harness against your chosen providers before you design your routing logic.
Enjoyed this deep dive? Join my inner circle:
- Pithy Cyborg → AI news made simple without hype.
- Pithy Security → Stay ahead of cybersecurity threats.
