API Performance
Server-side queries return in 2–7 ms p95. Reuse the TCP connection to keep end-to-end latency just as low.
2–7 ms
Server p95
5×
Pooled vs cold
~50 ms
Cross-region pooled
Where the milliseconds go
If you call the API more than once, the slowest part of the request is opening the connection — not the query itself.
| Component | Cost | Fix |
|---|---|---|
| Server work (MongoDB query + JSON encode) | 2–7 ms | — already optimized |
| Network round-trip (depends on geography) | 30–100 ms | Use closest region |
| TCP handshake (cold connection) | +30–50 ms | Reuse the connection |
| TLS handshake (cold HTTPS) | +50–100 ms | Reuse the connection |
| Response transfer (e.g. polygon ~100 KB) | 10–20 ms | gzip is automatic |
The takeaway: a fresh TCP+TLS handshake costs ~80–150 ms over the public internet. If your client opens a new connection per request, you pay that on every call. Reuse a single connection and you pay it once.
Latency by client pattern
| Pattern | First call | Subsequent calls |
|---|---|---|
| New connection per call | 200–300 ms | 200–300 ms |
| Pooled connection (cross-region) | 200–300 ms | 30–60 ms |
| Pooled connection (same region as origin) | 100–150 ms | 5–15 ms |
Code examples
Each example shows the right way (one shared client/session) and, where relevant, the wrong way.
Python — requests.Session
import requests
# One Session = one connection pool. Reuse it for the lifetime of your script.
session = requests.Session()
session.headers.update({"Authorization": "Bearer YOUR_API_KEY"})
for postcode in ["15236", "10115", "75001"]:
r = session.get(
"https://csv2geo.com/api/v1/divisions/by-postcode",
params={"code": postcode, "country": "GR", "include": "geometry"},
)
r.raise_for_status()
print(r.json()["result"]["name"])
Wrong:
for code in codes: requests.get(url, ...) — new TCP+TLS handshake every iteration.
Node.js — undici Agent
import { Agent, fetch } from "undici";
// Single agent shared across all requests.
const agent = new Agent({
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 600_000,
connections: 10,
});
async function lookup(postcode, country) {
const url = `https://csv2geo.com/api/v1/divisions/by-postcode?code=${postcode}&country=${country}&include=geometry`;
const r = await fetch(url, {
dispatcher: agent,
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
return r.json();
}
For axios, pass an http.Agent / https.Agent with keepAlive: true when you create the instance — reuse that one axios.create() instance.
PHP — Guzzle with shared client
use GuzzleHttp\Client;
// Reuse $client across requests. Don't `new Client()` inside a loop.
$client = new Client([
'base_uri' => 'https://csv2geo.com/api',
'headers' => ['Authorization' => 'Bearer YOUR_API_KEY'],
]);
foreach ($postcodes as $code) {
$r = $client->get('/v1/divisions/by-postcode', [
'query' => ['code' => $code, 'country' => 'GR', 'include' => 'geometry'],
]);
$data = json_decode($r->getBody(), true);
}
Go — http.Client with persistent Transport
import (
"net"
"net/http"
"time"
)
var client = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
},
}
Watch out: always call
resp.Body.Close(). If you don't, the connection isn't returned to the pool and keep-alive silently doesn't work.
cURL — HTTP/2 multiplexing
curl --http2 \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://csv2geo.com/api/v1/divisions/by-postcode?code=15236&country=GR" \
"https://csv2geo.com/api/v1/divisions/by-postcode?code=10115&country=DE" \
"https://csv2geo.com/api/v1/divisions/by-postcode?code=75001&country=FR"
Or skip the loop entirely: batch endpoints accept up to 10,000 addresses per call — one request, one connection, one handshake.
Common mistakes
- Creating a new HTTP client / session per request inside a loop — defeats pooling.
- Forgetting to close response bodies in Go — leaks the connection so it isn't returned to the pool.
- Using
requests.get(...)instead ofsession.get(...)in Python. - Sending a
Connection: closeheader — forces tear-down after each call. - Using a low-level
http.Getin Go without configuring a customTransportif you need long idle timeouts.