You see one slow request. The page hangs for nine seconds.
Then it recovers. Your p50 looks fine. Your users still felt the lie.
Free-tier latency is not a flat number. It's a queue. The queue is shared. Model and server tend to share the same lane. When the lane jams, both choke.
In this post you'll get a probe script, a coupling test, a decision table, and a five-question FAQ. Run them before you rewrite your stack.
The two clocks in every HTTP call
Every request has two visible times:
- Time to first byte (TTFB)
- Time to last byte (TTLB)
TTFB captures queueing. TTLB captures transfer plus server work.
If TTFB balloons while TTLB stays flat, your code is waiting for a slot.
If both balloon, the service itself is slow.
This distinction is your first weapon.
A probe that exposes the queue
Save this as probe_queue.py.
import concurrent.futures as cf
import urllib.request
import time
import statistics
URL = 'https://your-endpoint.example/ping'
N = 20
CONCURRENCY = 5
def probe(i):
t0 = time.perf_counter()
try:
with urllib.request.urlopen(URL, timeout=30) as r:
r.read()
return time.perf_counter() - t0
except Exception:
return time.perf_counter() - t0
with cf.ThreadPoolExecutor(max_workers=CONCURRENCY) as ex:
results = list(ex.map(probe, range(N)))
latencies = sorted(results)
print(f'p50: {statistics.median(latencies):.3f}s')
print(f'p90: {latencies[int(N * 0.9) - 1]:.3f}s')
print(f'p99: {latencies[int(N * 0.99) - 1]:.3f}s')
print('sorted:', ' '.join(f'{s:.2f}' for s in latencies))
Run it with:
python probe_queue.py
Run it three times. Different days. Different hours.
Record p99. Watch it jump.
How to read the output
Here is a synthetic output from a crowded shared queue:
p50: 0.42s
p90: 3.10s
p99: 14.80s
sorted: 0.21 0.33 0.40 0.44 0.51 0.62 0.89 1.70 2.90 4.80 14.80 15.10
See the gap between p50 and p99. That gap is a queue.
A dedicated system keeps p99 close to p50. A shared system rarely does.
Rule of thumb: if p99 is more than 3x p50, expect a queue.
Want one number? Compute the coefficient of variation:
mean = statistics.mean(latencies)
stdev = statistics.stdev(latencies)
print(f'CV: {stdev / mean:.2f}')
CV above 1.0 means heavy-tailed traffic. That's a queue signature.
The coupling test: model and server together
Now bring in a second endpoint. The myth says model and server are independent.
Let's probe them simultaneously.
# probe_both.py
import concurrent.futures as cf
import urllib.request
import time
import statistics
MODEL_URL = 'https://model-endpoint.example/ping'
SERVER_URL = 'https://server-endpoint.example/ping'
SAMPLES = 10
def measure(url):
t0 = time.perf_counter()
try:
with urllib.request.urlopen(url, timeout=30) as r:
r.read()
return time.perf_counter() - t0
except Exception:
return float('inf')
def one_sample(_):
return measure(MODEL_URL), measure(SERVER_URL)
with cf.ThreadPoolExecutor(max_workers=2) as ex:
pairs = list(ex.map(one_sample, range(SAMPLES)))
model = [p[0] for p in pairs]
server = [p[1] for p in pairs]
print('model:', ' '.join(f'{x:.2f}' for x in model))
print('server:', ' '.join(f'{x:.2f}' for x in server))
Run it during peak hours. Look at the pairs.
If they spike together, you've found shared admission control.
The five-question myth-busting FAQ
Q1: Is my free server down when it's slow?
No. It's queued.
A short wait usually restores it. A restart helps less than patience.
Q2: Should I blame my code first?
No. Run the probe first.
If TTFB stretches across many requests, your code is innocent.
Q3: Should I increase retries?
Only with exponential backoff and jitter.
Raw retries multiply queue pressure. Sleep 1s, 2s, 4s, add randomness.
Q4: Is a 95% success rate enough?
It depends on your users.
A chat app feels the tail. A nightly batch job doesn't.
Q5: Are free model and free server truly independent?
Usually not.
They often share the same lane. That's the myth this post is meant to kill.
A mental model that works
Stop picturing two separate products.
Think: one shared lane, two toll booths.
Lane empty? Fast lane.
Lane crowded? Both suffer.
Design for a crowded lane.
Decision table: should you use free tier?
| Use case | Free tier OK? | Why |
|---|---|---|
| Local experiments | ✅ | Latency doesn't matter |
| Low-volume internal tools | ⚠️ | Add timeouts and caching |
| User-facing chat | ❌ | Tail latency kills UX |
| Payments or auth | ❌ | Correctness beats savings |
| Scheduled offline batcher | ✅ | Queue-friendly and retryable |
If you see ❌, buy a dedicated plan or self-host.
One workflow that fits
Build batch jobs, not real-time calls.
Outline:
- Push tasks to a Redis queue.
- Worker picks one task.
- Worker calls the free model endpoint once.
- On timeout, worker sleeps 5–20s, then retries.
- Success writes to DB. Failure writes to dead-letter queue.
This pattern absorbs spikes. I use it for side projects.
MonkeyCode offers free model access and a free server option. That pair maps nicely to steps 3 and 2.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Who should not use this approach
Skip this pattern if you:
- Have a strict response-time SLA
- Process payments or auth
- Demo live to a client
- Face a customer support queue
Free tier is a lab, not a luxury box.
Final thought
Measure first. Blame second.
Probe the queue. Respect the tail.
Then decide if free tier fits your workload.
That's all. Go run probe_queue.py.
Top comments (0)