Your service calls a free model endpoint. The client timeout is 30 seconds. The model answers in 35.
Who pays the extra five seconds?
Your user does. So does every service behind you.
I run small services against free model servers. MonkeyCode's free model access and free server option keep the barrier low. Low cost is good. Low cost is not low complexity.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
This post is a myth-busting FAQ. Each myth is a claim I keep seeing in code. Each myth gets evidence you can reproduce. There is a runnable probe at the end.
The Myths, In One List
- A client timeout is a deadline.
- Longer timeouts are safer.
- Only the model call needs a timeout.
- A connect timeout is enough.
Here is the corrected mental model: timeouts are local limits. Deadlines are end-to-end promises. You need both. Most code only has the first.
Myth 1: A Client Timeout Is a Deadline
What I hear: 'The timeout is 30s, so the function can't hang.'
True. A function cannot hang forever. That is a weak guarantee.
A deadline comes from outside. A user waits two seconds. A queue pops after five. An upstream SLO fails at ten. Your function has no opinion about any of those.
Now chain a few services:
- Service A waits for Service B. Timeout: 30s.
- Service B waits for the model. Timeout: 30s.
- The model is slow. It takes 35s.
What happens? The model finishes at 35s. B abandoned the call at 30s. B returns an error. A waits for a useful answer but now has no time left. The user left after 10s.
The timeout protected nobody. It was local. The deadline was global.
Correction: name the deadline first. Then divide it into slices. Only then pick relative timeouts.
Myth 2: Longer Timeouts Are Safer
What I hear: 'Free servers are slow. Give them a full minute.'
Run the arithmetic. Two nested calls, 60s each. Worst case before your code learns: 120 seconds.
That 120-second window is not free. You hold a connection. You hold a worker. You might hold a database handle. Queueing delays multiply downstream.
A generous timeout does not fix a slow tail. It just waits for the whole tail.
Correction: a timeout is a budget cap. Want to be safer? Fail faster with a useful fallback. A clear 504 beats a late 200.
Myth 3: Only the Model Call Needs a Timeout
What I hear: 'Everything around the call is fast. DNS, JSON, logs — no risk.'
Really? DNS can hang. A cold database can hang. Logs can block under pressure.
Every hop between request and response is latency. None of them is protected by the model timeout.
Correction: measure every hop. Allocate each one a slice. Slice 1: DNS lookup. Slice 2: preprocessing. Slice 3: the model call. Slice 4: validation and response write.
Slices are not guesses. They come from a probe.
Myth 4: A Connect Timeout Is Enough
What I hear: 'If the server is down, connect fails. Timeout on connect only.'
Here is the gap: a server can accept a TCP connection instantly. Then it thinks for a long time. Then it sends the first token.
Or it never sends anything.
Your connect timeout never fires. Your code sleeps on the read. Nobody set a read timeout.
Correction: timeouts on connect, read, and write are separate. A total budget is separate again. In async Python, wrap the whole step in asyncio.timeout.
The Mental Model That Fixes All Four
Write it down before you code.
- A request arrives. Set a deadline:
request_start + 2.0s. - Pass the deadline down the stack as an absolute time. Relative timeouts become slices.
- Each hop asks: How much budget is left?
- If the answer is zero, stop. Return 504 with the reason.
Here is a small async template:
import asyncio
from time import monotonic
class BudgetExhausted(Exception):
pass
def remaining(deadline: float) -> float:
return deadline - monotonic()
async def call_model(client, payload: dict, deadline: float):
left = remaining(deadline)
if left <= 0:
raise BudgetExhausted()
# The whole I/O step gets one slice of the budget.
async with asyncio.timeout(left):
return await client.post('/chat', json=payload)
Not magic. The template forces one question: How much time is really left? Relative timeouts never ask that.
A Probe You Can Run
This is the reproducible part. Point it at any HTTP endpoint you use. It warms up and prints per-hop times. You decide the slices from the output, not from a blog post.
import asyncio
import json
import os
import time
import urllib.request
URL = os.environ.get('MODEL_URL', 'http://127.0.0.1:8000/chat')
def one_call(payload: dict) -> float:
data = json.dumps(payload).encode()
req = urllib.request.Request(URL, data=data, method='POST')
t0 = time.perf_counter()
with urllib.request.urlopen(req, timeout=30) as resp:
resp.read()
return time.perf_counter() - t0
async def main():
payload = {'prompt': 'ping', 'max_tokens': 8}
# Warm-up beats first-call surprises.
for _ in range(3):
one_call(payload)
times = sorted(one_call(payload) for _ in range(10))
median = times[len(times) // 2]
p90 = times[int(len(times) * 0.9) - 1]
print(f'median={median*1000:.0f}ms p90={p90*1000:.0f}ms')
asyncio.run(main())
Change the URL. Change the payload. Run it twenty times.
If your p90 is triple your median, a fixed 30-second client timeout is a lottery ticket. You want a deadline budget that includes the tail.
Decision Table: When the Budget Runs Out
The hard part of a deadline budget is the response.
| Situation | Evidence in your probe | Action |
|---|---|---|
| Connect fails fast | OSError within 50ms | Retry once. Connect failures are often transient. |
| Read times out, first token arrived | Partial body streamed | Do not retry. State is ambiguous. Return 504. |
| Read times out, nothing arrived | 0 bytes in the window | Retry once only if the caller allows it. |
| Total budget exhausted | Median and p90 confirm it | Stop. Degrade to a fallback or an error. |
The table keeps decisions where the data lives. This script measures. It does not retry. Recovery is where your product policy starts.
Who Should Skip This Approach
This is not a universal law. Skip it when:
- You run one-shot batch jobs. A 60-second wait is fine at 2 AM.
- You have no user-facing request. No deadline exists.
- Your chain has one hop. One timeout equals one deadline.
Otherwise the claim holds: your code needs deadlines, not just timeouts. Free model servers are a great place to practice, because the tail is visible. If you handle the budget there, your paid-tier calls will feel boring — and boring is good.
Next time you adjust a timeout, write the deadline down in code. Then time every hop. The answer often surprises you. It will also surprise your users less.
Top comments (0)