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
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.
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 PostgreSQLLISTEN/NOTIFYon theproject_route_changechannel (RouteTableListener+ProjectChangeListener). - The console publishes PG
NOTIFYafter every deployment, domain change, or environment state change. - The in-process
Job::ForceRouteReloadpath 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.
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.
Split mode (temps proxy) is available today and uses PG NOTIFY for route table synchronisation. The --role=console flag and the automated upgrade tooling are under active development (ADR-017). For most deployments, combined mode (temps serve) is the right choice.
Request lifecycle
Complete Request Flow
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
ForceRouteReloadwake 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:
Resolution Steps
- Extract Hostname: From
Hostheader or TLS SNI - Domain Lookup: Query
domainstable for matching domain - Project Resolution: Get associated project and environment
- Deployment Selection: Find active deployment for environment
- Upstream Selection: Determine container endpoint to forward to
Fallback Behavior
If domain resolution fails:
- Return
503 Service Unavailableif domain not found - Return
403 Forbiddenif domain exists but project is disabled - Log error for debugging
503 responses generated by the proxy do not include internal error detail in the response body. Error strings are written to the proxy log only — no diagnostic information is leaked to the client.
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
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 mode | Split mode | |
|---|---|---|
| Route update mechanism | In-process Job::ForceRouteReload | PG NOTIFY project_route_change |
| Typical propagation delay | sub-millisecond | 100–400 ms |
| Resilience to LISTEN drop | Not applicable (no NOTIFY needed) | Relies on reconnect |
| On-demand wake signal | In-process ForceRouteReload | PG job queue + NOTIFY |
Security checks
IP Access Control
Before forwarding requests, Temps checks IP access control rules:
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:
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 requestX-Project-ID: Project identifierX-Environment-ID: Environment identifierX-Client-IP: Client IP addressX-Forwarded-For: Original client IP (if behind proxy)X-Forwarded-Proto: Original protocol (http/https)