Request Flow

Every request to a Temps-hosted application passes through a Pingora proxy pipeline that handles TLS termination, route resolution, security checks, and analytics before your container sees a single byte.


Overview

Every request to your Temps-hosted application goes through a well-defined pipeline that handles TLS termination, routing, security checks, and analytics.

High-Level Flow

Loading diagram...

Process modes

Temps can run as a single combined process or as two separate processes. The mode you are running determines how the proxy and the console coordinate on route table updates, on-demand wakes, and state sharing.

Combined Mode (default)

temps serve starts both the Pingora proxy (:80/:443) and the console API on the same OS process. They share an in-process tokio broadcast queue (BroadcastQueueService). Route table updates are delivered immediately through Job::ForceRouteReload dispatched over that queue, so freshly deployed environments become routable in milliseconds without any network round-trip.

Loading diagram...

Split Mode (temps proxy + temps serve --role=console)

In split mode the Pingora proxy runs as a separate OS process (temps proxy) while the console API runs as temps serve --role=console. The two processes share no memory; all coordination goes through PostgreSQL:

  • The proxy reads the route table from an in-memory cache (CachedPeerTable) that is refreshed via PostgreSQL LISTEN/NOTIFY on the project_route_change channel (RouteTableListener + ProjectChangeListener).
  • The console publishes PG NOTIFY after every deployment, domain change, or environment state change.
  • The in-process Job::ForceRouteReload path is inert in split mode — the proxy and console run separate queues, so the job never crosses the process boundary. The proxy falls back to the PG NOTIFY path, which introduces an additional propagation delay (typically 100–400 ms) compared to the sub-millisecond in-process path in combined mode.
Loading diagram...

The primary motivation for split mode is zero-downtime console upgrades: restarting the console process does not interrupt traffic on :80/:443 because Pingora holds the listening socket in the proxy process. Schema migrations touching tables that the proxy reads must remain backward-compatible with the running proxy binary for the duration of the upgrade window.


Request lifecycle

Complete Request Flow

Loading diagram...

Processing phases

Temps uses Pingora's ProxyHttp trait, which provides 6 phases for request handling:

Phase 1: Early (select_upstream)

Purpose: Determine which deployment should handle this request, waking sleeping environments if needed.

Actions:

  • Extract hostname from request
  • Resolve domain to project and environment
  • Check IP access control rules
  • Determine if CAPTCHA challenge is required
  • Check whether the resolved environment is running or sleeping
  • If sleeping: trigger an in-process ForceRouteReload wake sequence (not a PG NOTIFY) to start the container, then run a TCP readiness probe that holds the request until the container port is open and accepting connections
  • Select upstream deployment

Outcome: Upstream selected, or an error response returned to the client. Sleeping environments are transparently woken; the first request waits in-process until the container is ready rather than receiving an immediate error.

Phase 2: Modify (request_filter)

Purpose: Modify the request before forwarding.

Actions:

  • Add Temps-specific headers (X-Request-ID, X-Project-ID, etc.)
  • Extract request metadata (method, path, query string, user agent)
  • Extract client IP address
  • Compress request body if needed
  • Set request start time for performance tracking

Outcome: Modified request ready for forwarding.

Phase 3: Proxy (upstream_peer)

Purpose: Establish connection to your deployment.

Actions:

  • Connect to deployment container
  • Forward modified request
  • Handle connection errors and timeouts

Outcome: Request sent to your application.

Phase 4: Response (upstream_response_filter)

Purpose: Process response from your deployment.

Actions:

  • Decompress response if needed
  • Modify response headers
  • Add security headers
  • Handle response errors

Outcome: Processed response ready to send to client.

Phase 5: Filter (response_filter)

Purpose: Final response modifications.

Actions:

  • Add analytics tracking pixels (if enabled)
  • Inject monitoring scripts (if enabled)
  • Final header modifications

Outcome: Final response ready.

Phase 6: Finish (logging)

Purpose: Log request for analytics and monitoring.

Actions:

  • Create analytics event with request metadata
  • Record performance metrics (response time, status code)
  • Save to database for dashboard and reports

Outcome: Request logged and metrics recorded.


Project resolution

Domain to Project Mapping

When a request arrives, Temps must determine which project and environment should handle it:

Loading diagram...

Resolution Steps

  1. Extract Hostname: From Host header or TLS SNI
  2. Domain Lookup: Query domains table for matching domain
  3. Project Resolution: Get associated project and environment
  4. Deployment Selection: Find active deployment for environment
  5. Upstream Selection: Determine container endpoint to forward to

Fallback Behavior

If domain resolution fails:

  • Return 503 Service Unavailable if domain not found
  • Return 403 Forbidden if domain exists but project is disabled
  • Log error for debugging

On-demand environments

On-demand (scale-to-zero) environments sleep when idle and wake automatically on the first incoming request. The wake sequence runs entirely inside the proxy's request_filter phase so the client connection is held open rather than immediately rejected.

Wake Sequence

Loading diagram...

First-Request Latency

The wake sequence adds latency to the first request hitting a sleeping environment:

  • Container start time — typically 1–5 s depending on image size and host.
  • Route propagation — in combined mode the route becomes routable immediately after ForceRouteReload; in split mode an additional PG NOTIFY round-trip (100–400 ms) is required before the proxy's cache reflects the new route.
  • Bounded re-resolve — after the wake signal the proxy retries route resolution in a short loop (up to ~5 s in 100 ms increments). If the route is available the request proceeds; if not, the proxy returns a retryable 503 wake_pending.

Wake in Split Mode

In split mode the console process is responsible for container lifecycle management. When the proxy receives a request for a sleeping environment it publishes a wake signal via the PostgreSQL job queue so the console can act on it, then waits for the route to appear in its local cache via PG NOTIFY. There is no direct cross-process function call.

If the proxy cannot publish a wake signal (for example because the database is temporarily unreachable) it returns a 503 immediately rather than holding the connection indefinitely. The client can retry once the database connection is restored.

Concurrency Protection

To prevent a flood of incoming requests from spawning multiple redundant wake attempts, the OnDemandManager gates concurrent wakes with a semaphore. Requests that cannot acquire a wake slot receive an immediate 503 wake_throttled response instead of being queued, which caps memory and file-descriptor consumption in the Pingora hot path during traffic spikes on scale-to-zero environments.


Route table propagation

The proxy maintains an in-memory route table (CachedPeerTable) that maps hostnames to upstream containers. How that table stays fresh depends on the process mode.

Combined Mode

After a deployment completes, the console publishes Job::ForceRouteReload on the shared in-process broadcast queue. RouteReloadSubscriber picks it up, calls load_routes() (a full authoritative reload from the database), and publishes a RouteTableUpdated confirmation. The deploy pipeline waits for that confirmation before tearing down the old containers, so the route is guaranteed to be live before the old container disappears.

This path is deterministic and does not rely on PG NOTIFY — it works even if the PostgreSQL LISTEN connection is temporarily dropped.

Split Mode

In split mode the proxy's RouteReloadSubscriber receives no in-process jobs from the console. Route updates reach the proxy exclusively via the PostgreSQL project_route_change NOTIFY channel, handled by RouteTableListener and ProjectChangeListener. The listener holds a persistent LISTEN connection; if that connection is silently dropped by a load balancer or NAT idle-timeout the next NOTIFY will be missed until the connection is re-established.

The practical implication: a freshly deployed environment in split mode may take up to a few hundred milliseconds longer to become routable compared to combined mode, and in pathological LISTEN-drop scenarios the delay could be longer until the connection recovers. Combined mode is recommended for deployments where sub-second route propagation matters.

Summary

Combined modeSplit mode
Route update mechanismIn-process Job::ForceRouteReloadPG NOTIFY project_route_change
Typical propagation delaysub-millisecond100–400 ms
Resilience to LISTEN dropNot applicable (no NOTIFY needed)Relies on reconnect
On-demand wake signalIn-process ForceRouteReloadPG job queue + NOTIFY

Security checks

IP Access Control

Before forwarding requests, Temps checks IP access control rules:

Loading diagram...

IP Rules:

  • Allow List: Only specified IPs can access
  • Block List: Specified IPs are denied
  • Default: All IPs allowed (if no rules configured)

CAPTCHA Challenge

For projects with CAPTCHA enabled, requests may be challenged:

Loading diagram...

CAPTCHA Triggers:

  • First request from new IP
  • Suspicious traffic patterns
  • Rate limiting violations
  • Manual enablement by project owner

Request Headers Added

Temps automatically adds headers to requests forwarded to your deployment:

  • X-Request-ID: Unique identifier for this request
  • X-Project-ID: Project identifier
  • X-Environment-ID: Environment identifier
  • X-Client-IP: Client IP address
  • X-Forwarded-For: Original client IP (if behind proxy)
  • X-Forwarded-Proto: Original protocol (http/https)

Was this page helpful?