The web UI loads, the API errors: three differences
Opening an AI web UI in a browser means one request that finishes and ends; the browser reuses connections for you, and if it fails you just refresh. API calls don't work that way: the SDK keeps sending requests in the background, streaming output has to hold a long-lived connection for tens of seconds or more, and several tasks may arrive at once. These three differences are the easiest to overlook — and the ones that surface once batch jobs really start running.
| What to watch | Web UI | API calls | Typical symptom |
|---|---|---|---|
| Egress IP | Changes are barely noticeable | Should stay as static as possible | 401 / 403, region-restricted errors |
| Concurrent connections | Managed by the browser connection pool | One connection per task, opened at the same time | Connection timeouts, 429, EOF |
| Timeout settings | Ends once the page has loaded | Must cover the full length of the streaming response | read timeout, connection reset |
Start troubleshooting with the egress: if the egress is wrong, the results of the other two checks are contaminated.
Egress IP: APIs care more about static than web UIs do
A web UI identifies you by cookie or session, so a different exit address usually just means verifying again. APIs are different: the platform ties region and risk-control policies to the API key, and an exit address that jumps around is easily flagged as an anomalous source — at best you're asked to verify again, at worst requests are rejected outright.
Shared egress vs. static egress
- Shared egress: several users share one exit address, so their behaviour affects each other and problems are harder to pin down.
- Static egress: IEPL lines usually provide a stable exit address, which suits cases where the address has to be added to a platform allowlist.
- With relay routes the exit is decided by scheduling, so test in your own environment whether it stays static — don't go by the description alone.
Verify that the egress is stable
- Print the egress address on your local dev machine and on the server that runs the jobs, and record the results.
- Repeat at intervals and check whether the records match; for batch jobs, test again during the hours they actually run.
- Check which side resolves the domain name — don't let the resolution result and the egress address disagree.
- Add the API domains you call to the proxy rules so requests really go through the proxy instead of being let through as direct traffic by a routing rule.
# Print the current egress address
curl -sS https://ipinfo.io/ip
# Print connection time and time to first byte to tell "can't connect" from "connected but slow"
curl -sS -o /dev/null \
-w "connect=%{time_connect} start=%{time_starttransfer}\n" \
-H "Authorization: Bearer $API_KEY" \
https://api.openai.com/v1/models
Which side handles DNS resolution
If the proxy only handles TCP and domain resolution still happens locally, the platform may see a resolution location that doesn't match the egress address, and some endpoints return region errors. Turn on remote resolution in the client, or confirm the API domain matches a proxy rule, before troubleshooting further.
Concurrency: know the three limits first
When concurrency won't go up, it's usually not local bandwidth — one of three limits is hit first.
- Client limits: file descriptors per process, HTTP connection pool size, thread or coroutine count.
- Route and NAT session limits: one egress address can only hold a finite number of sessions at a time.
- Platform-side rate limits: a cap on concurrent requests per API key or per egress address, usually returned as 429.
HTTP/2 multiplexing isn't a cure-all
HTTP/2 lets one TLS connection carry several requests, saving repeated handshakes. But the server uses SETTINGS_MAX_CONCURRENT_STREAMS to cap concurrent streams on a single connection, and anything beyond that is queued rather than failing immediately. The result: no errors, but wait times keep growing, nothing obvious shows up in the logs, and only metrics reveal it.
How to tune concurrency
- Use a semaphore or connection pool to keep in-flight requests within an observable range, and ramp up from a low concurrency.
- Retries need exponential backoff and jitter, so failed tasks don't all retry in the same second.
- When you need more concurrency, spread tasks across several routes instead of saturating one.
- Emit metrics for concurrency, failure rate and average wait time, so you have evidence when something goes wrong.
Timeouts: set streaming by idle gap, not total duration
A timeout isn't one number but three kinds, each doing a different job.
- Connect timeout: from sending the request to completing the handshake. Keep it short — a timeout here usually means the route is down or the egress is unreachable.
- Read timeout (read / idle): the longest wait for the next chunk. For streaming, set it to the longest gap between two chunks.
- Total timeout: a hard cap on the whole request. Only suitable for non-streaming requests; on streaming requests it kills legitimate long output.
The default read timeout in many SDKs is fine for short requests but too tight for models that stay silent for a long time during inference: the connection looks idle but isn't actually broken, and the client cuts it off, leaving nothing in the log but a read timeout.
The cost of retries
Once a streaming response has started returning content, retrying can cause duplicate billing and scramble the context. Limit retries to connection setup, and to clear 5xx and 429 responses; for 429, back off before trying again.
A workable timeout configuration approach
import httpx
from openai import OpenAI
client = OpenAI(
base_url="https://api.openai.com/v1",
# Set read to the longest gap between two chunks, not the whole request duration
timeout=httpx.Timeout(connect=5.0, read=180.0, write=30.0, pool=5.0),
max_retries=2,
)
For streaming endpoints keep only the idle check and relax the total timeout to the upper limit the business allows; for non-streaming batch jobs it's the other way round — a clear total timeout is safer.
How to choose a route: IEPL, relay and direct
The three route types don't replace each other — they fit different call patterns.
| Route type | Path characteristics | Best fit | Check before you start |
|---|---|---|---|
| IEPL dedicated line | Carried end to end on a dedicated line, with no public-internet detours | Long-lived streaming connections, needs a static egress | Whether the exit address is static and can be allowlisted |
| Relay | Connects to a relay node before the international leg | Higher concurrency, cost-sensitive batch jobs | Whether the exit changes with scheduling |
| Direct | Connects straight to overseas nodes | Debugging, lightweight requests | The public-internet path is more exposed to congestion |
VPNFN covers 120+ countries / regions and 180+ routes, all three route types are available, and the privacy policy is no-logs. A sensible order: get features working over a direct connection first, then move calls that need a stable egress to an IEPL line, and put retryable batch jobs on relay routes so each kind of request takes its own path.
Plans and data: pick a tier by call volume
Monthly subscriptions come in three tiers: 60GB / 250GB / 500GB. API traffic costs come from two places: long-context requests noticeably inflate upload traffic, and streaming output counts downstream by the tokens actually generated. To estimate, multiply the upload and download size of one request by daily calls, times 30 days, then leave headroom.
- Debugging and personal scripts: start with the smallest tier and get egress, concurrency and timeouts working before worrying about traffic.
- Long batch jobs: estimate monthly traffic as above, then decide whether to move up a tier or add a data pack.
- Highly variable usage: data packs are used up rather than expiring, so they work well as a buffer on top of a monthly subscription.
- No device limit — dev machines, servers and phones can be online at the same time, with no separate plan for each machine.
Bottom line: start with the smallest monthly tier to verify whether the egress is static, how much concurrency you can reach and how timeouts should be set; once all three check out, move up a tier based on real usage, or use a data pack to absorb spikes.
Pre-launch checklist
Run through the items below; they cover the most common connection failures in AI API calls.
- ✅ Print the egress address several times in a row and confirm the dev machine and the server see the same one.
- ✅ Set connect and read timeouts explicitly in the SDK instead of relying on defaults.
- ✅ Set the read timeout for streaming requests by idle gap, and handle models that stay silent for a long time during inference separately.
- ✅ Limit retries to connection setup, 5xx and 429, with exponential backoff and jitter.
- ✅ Add the API domains to the proxy rules and confirm DNS resolution happens remotely.
- ❌ Don't hard-code the address of one route in your code; the subscription link should be updatable as a whole.
- ❌ Don't commit API keys together with proxy config to the code repository.
In short: most stability problems with AI APIs aren't in the calling code — they come down to three things: whether the egress address is stable, whether concurrency is being throttled, and whether timeouts cover the streaming response. Verify these three first, then talk about routes and plans.