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 of session.get(...) in Python.
  • Sending a Connection: close header — forces tear-down after each call.
  • Using a low-level http.Get in Go without configuring a custom Transport if you need long idle timeouts.