Skip to content

Request Flow

This page traces an HTTP request from the moment it arrives at the ingress listener to the moment the response is delivered back to the client.

sequenceDiagram
participant Client
participant Ingress as Ingress Listener
participant RL as Rate Limiter
participant Sem as Connection Semaphore
participant Body as Body Validator
participant Router as Ingress Router
participant Registry as Function Registry
participant Pool as Isolate Pool
participant Thread as Isolate Thread (V8)
participant Handler as JS/TS Handler
Client->>Ingress: HTTP request
Ingress->>RL: Check rate limit
RL-->>Ingress: Allow / Deny (429)
Ingress->>Sem: Acquire connection permit
Sem-->>Ingress: Permit granted / Rejected (503)
Ingress->>Body: Validate body size
Body-->>Ingress: OK / Rejected (413)
Ingress->>Router: Forward request
Router->>Router: Extract function name from first path segment
Router->>Registry: Lookup function
Registry-->>Router: Function handle / Not Found (404)
Router->>Pool: Request isolate for function
alt Warm isolate available
Pool-->>Router: Return warm isolate
else No warm isolate
Pool->>Thread: Spawn new isolate (cold start)
Thread-->>Pool: Isolate ready
Pool-->>Router: Return new isolate
end
Router->>Thread: Dispatch request to isolate thread
Thread->>Handler: Invoke JS/TS handler
Handler-->>Thread: Response object
Thread-->>Ingress: Stream response
Ingress-->>Client: HTTP response
Thread->>Thread: Resource cleanup
Thread->>Pool: Return isolate to pool

The ingress listener accepts the TCP connection (plain or TLS). If TLS is configured, the DynamicTlsAcceptor performs the handshake using the current certificate.

A token-bucket rate limiter keyed by source IP decides whether the request is allowed. If the bucket is exhausted, the server responds immediately with 429 Too Many Requests.

A bounded semaphore limits the total number of concurrent in-flight requests. If no permit is available, the server responds with 503 Service Unavailable. This protects downstream isolates from being overwhelmed.

The Content-Length header (or streaming body size) is checked against the configured maximum. Oversized payloads are rejected with 413 Payload Too Large before any bytes are buffered into memory.

The ingress router extracts the function name from the first segment of the request path. For example, a request to /my-function/api/hello resolves to the function named my-function. The remainder of the path (/api/hello) is forwarded to the handler as the local path.

The FunctionRegistry is consulted using a lock-free DashMap read. If no function is registered under the extracted name, the server responds with 404 Not Found.

The isolate pool attempts to hand out a warm (already-initialized) isolate for the target function using LRU eviction policy. If none is available, a cold start is triggered.

The request is sent to the isolate’s dedicated OS thread via a channel. The V8 event loop on that thread picks up the request and invokes the JavaScript or TypeScript handler.

User code runs inside the V8 sandbox. It has access to the standard Web APIs (fetch, crypto, TextEncoder, etc.), the Thunder runtime APIs, and the Node.js polyfills registered at boot time.

The handler returns a Response object. The body is streamed back to the client through the Hyper layer without buffering the entire payload in memory.

After the response is fully sent, per-request resources (timers, open fetch connections, pending futures) are cleaned up. The isolate is returned to the pool for reuse.

AspectCold startWarm start
TriggerNo idle isolate in the pool for the target functionIdle isolate available in the pool
Work performedSpawn OS thread, create V8 isolate, load ESZIP, run bootstrap.js, compile module graphReuse existing thread and isolate, reset per-request state
Typical latency10—100 ms depending on bundle sizeSub-millisecond dispatch
When it happensFirst request after deploy, after eviction, or after scale-to-zeroSubsequent requests within the idle timeout

The pool uses an LRU eviction strategy: when the pool reaches its capacity limit, the least-recently-used isolate is terminated to make room for a new one.

Thunder enforces wall-clock timeouts on handler execution using a watchdog thread pattern:

  1. When a request is dispatched to an isolate, a watchdog timer is started on a separate thread.
  2. If the handler completes before the deadline, the watchdog is cancelled.
  3. If the deadline expires, the watchdog thread terminates the V8 isolate by calling isolate.terminate_execution(). This causes the handler to throw an unrecoverable exception.
  4. The server responds with 504 Gateway Timeout and the isolate is discarded (not returned to the pool).

This two-thread approach (isolate thread + watchdog thread) ensures that even infinite loops or blocked synchronous code cannot prevent the timeout from firing.