From 4 round-trips to 1: warm EPP pooling on serverless
June 25, 2026 · RestEpp team
EPP is a session protocol: you <login>, then you send commands over the
same TLS socket, then eventually you <logout>. That’s a fine model for a
long-running registrar client. It’s an awkward fit for a stateless HTTP
function that’s supposed to answer one request and (conceptually) forget
everything.
The naive implementation — connect, login, run one command, disconnect, on
every single API call — is correct but slow. In our measurements against a
live sandbox, a cold connect + login + checkDomain + disconnect cycle took
10.9 seconds. Doing the same command against an already-warm session took
1.0 second. That’s not a rounding error; it’s the difference between a
usable API and a frustrating one.
The pool
RestEpp keeps a module-scope map of live EPP sessions, keyed by credHash —
the same hash used to bind one API key to one registry identity (see the key
design post):
Map<credHash, { client: EppClient, lastUsed: number, lock: Promise }>
On each authenticated request:
- If there’s no entry for this
credHash, or the existing client’sisAlive()check fails, open a fresh connection and log in. - Requests for the same
credHashare serialized through a chained promise lock — EPP sessions aren’t safe for concurrent commands on one socket, so we queue rather than race. - Run the command, update
lastUsed. - If a command fails because the socket died mid-flight, evict the entry, reconnect once, and retry — transparently, from the caller’s perspective.
Idle entries are swept and disconnected after a TTL (5–10 minutes) so we’re
not holding registry sessions open indefinitely. Critically, eviction is a
plain disconnect() — we don’t send <logout> on eviction, since that’s
a registry-side session teardown we don’t need to pay for on every idle
sweep.
Why this works on serverless
Function instances aren’t guaranteed to survive between invocations, but within a warm instance, module-scope state persists across requests handled by that instance. So the pool isn’t a cache we hope survives — it’s a correctness-preserving optimization: worst case (cold instance), you pay the 10.9s connect+login once; best case (warm instance, same credential), subsequent calls hit the 1.0s path.
In production this means: your first call after a deploy or scale-up event is slower, and every call after that — from any client using the same registry credential, routed to that warm instance — is fast.
What we deliberately didn’t do
We didn’t reach for a connection pool library or an external session store (e.g., Redis-backed EPP sessions shared across instances). EPP sockets are stateful TCP connections; you can’t hand one off between processes. The pool is intentionally per-instance, in-memory, and simple — the complexity budget went into correctness (the serialize-per-credHash lock, the reconnect-on-dead-socket path) rather than trying to defeat the stateless nature of the runtime.