You got a 429. Or a timeout. Was it your code? Or their quota?
Free model servers throttle quietly. Sometimes they never send a 429 at all. They just slow down. I spent weeks probing free endpoints. The most useful signal wasn't the response body. It was the headers.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. I use MonkeyCode's free model access and free server option in this workflow. The method works on any free endpoint.
Why Headers Beat Guessing
Most rate limiters leave fingerprints. RateLimit-Remaining tells you how many calls are left. RateLimit-Reset tells you when the window clears. Retry-After tells you how long to wait.
Some servers send nothing. That's a signal too.
The problem? Header formats are inconsistent. Some use X-RateLimit-*. Some use RateLimit-*. Some rename everything.
You need a probe. Not a guess.
The Probe
Here's the script I run. It sends one request every two seconds. It logs status, latency, and every rate-limit header it finds.
import csv
import time
import requests
from datetime import datetime, timezone
URL = "https://your-free-endpoint.example/v1/chat/completions"
TOKEN = "your-token"
INTERVAL_SECONDS = 2
MAX_CALLS = 60
headers = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
payload = {
"model": "your-model",
"messages": [{"role": "user", "content": "Reply with the word ok."}],
"max_tokens": 10,
}
RATE_KEYS = [
"ratelimit-limit", "ratelimit-remaining", "ratelimit-reset",
"x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset",
"retry-after", "x-ratelimit-retry-after",
]
def rate_headers(resp):
return {k: resp.headers.get(k) for k in RATE_KEYS if resp.headers.get(k)}
with open("probe.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["ts", "status", "latency_ms",
"limit", "remaining", "reset", "retry_after"])
for i in range(MAX_CALLS):
start = time.perf_counter()
try:
resp = requests.post(URL, headers=headers, json=payload, timeout=30)
latency = int((time.perf_counter() - start) * 1000)
h = rate_headers(resp)
writer.writerow([
datetime.now(timezone.utc).isoformat(),
resp.status_code, latency,
h.get("ratelimit-limit", ""),
h.get("ratelimit-remaining", ""),
h.get("ratelimit-reset", ""),
h.get("retry-after", ""),
])
print(i, resp.status_code, latency, h)
except requests.exceptions.Timeout:
writer.writerow([datetime.now(timezone.utc).isoformat(),
"timeout", "", "", "", "", ""])
print(i, "timeout")
except requests.exceptions.ConnectionError:
writer.writerow([datetime.now(timezone.utc).isoformat(),
"conn_error", "", "", "", "", ""])
print(i, "conn_error")
time.sleep(INTERVAL_SECONDS)
That's it. Sixty calls. Two minutes. One CSV.
How to Read the CSV
Open it. Sort by timestamp. Look for patterns.
column -s, -t < probe.csv | head -25
Then bucket every row:
import csv
from collections import Counter
rows = list(csv.DictReader(open("probe.csv")))
print(Counter(r["status"] for r in rows))
Now ask three questions.
Does remaining hit zero before a 429? If yes, the limiter is honest. Back off until reset.
Does retry-after match reality? Sleep exactly that long. If the next call still fails, the header is a lie.
Does latency climb while status stays 200? That's queueing, not quota. Your fix is concurrency, not waiting.
The Decision Table
| Header pattern | What it means | What I do |
|---|---|---|
remaining drops to 0 before any 429 |
Honest fixed window | Wait until reset, then resume |
429 + retry-after
|
Recoverable throttle | Sleep retry-after seconds |
| 429 with no headers | Opaque throttle | Back off 60s, log it, alert |
| 200 but latency climbs | Server queueing | Cut concurrency, not rate |
reset changes between calls |
Rolling window | Re-read headers every call |
| Headers vanish under load | Limiter hides itself | Treat every call as possibly throttled |
Keep this table next to your probe. It turns noise into decisions.
Vary the Probe
One fixed interval is not enough. Change four things.
- Interval. Run at 1s, 5s, and 30s. Fast intervals expose the limiter. Slow intervals expose reset behavior.
- Payload size. Send 50 tokens. Then send 2000. Some servers throttle on tokens, not calls.
- Concurrency. Run two probes in parallel. Watch for queueing.
- Time of day. Free servers share capacity. Evening load changes everything.
Each variation is one CSV. Keep them in separate files. Compare the header columns, not just the status codes.
Where Free Servers Perform Well
Free endpoints are honest more often than you'd think. Many send RateLimit-* headers on every response. That's rare on paid tiers.
A predictable reset window is gold. You can schedule batch jobs around it. You can pre-warm before the window clears.
The probe reveals this in minutes. No support ticket needed.
Where Free Servers Break
Free servers break in three places.
First, header drift. The limit header says 60. The server throttles at 40. The header was decorative.
Second, silent queueing. Status stays 200. Latency triples. Your timeout fires and you blame the network. The headers tell the truth.
Third, reset lies. The header says reset in 30 seconds. You wait. The next 429 arrives anyway. The server reset its own clock.
None of these show up in a single test. That's why you log every call.
When I see a broken pattern, I follow three steps. First, reproduce with a single call. Second, check the raw headers with curl -i. Third, compare against the CSV.
curl -i -X POST "$URL" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"your-model","messages":[{"role":"user","content":"hi"}]}'
Limitations
This probe maps one endpoint, one model, one time of day. It doesn't map the whole system.
Header formats aren't standardized. Your endpoint may use none of the keys I listed. Check the raw response first.
A free server has no SLA. This probe tells you what the server does today. It doesn't promise tomorrow.
Who Should Skip This
Skip this if you have a paid contract with a real SLA. Skip it if you need hard guarantees. Skip it if one failed call costs money.
This is for the rest of us. The ones building on free tiers. The ones who need to know where the edge is before we fall off it.
Run the probe. Read the headers. Then tell me what your server lied about.
Top comments (0)