Inside a Gortex request.
Five hot-path stages. The data plane underneath.
What happens between a request hitting the API and the response returning is a lifecycle worth being honest about, five synchronous stages running under a strict latency budget, with an asynchronous data plane writing trails behind every decision. What follows is the actual architecture as it ships today, with status tags on each stage so it's clear what's live versus what's being built.
Five stages. ~100ms cumulative typical. 200ms p99 budget.
Every request flows through these five stages in sequence. Each stage owns a slice of the total latency budget; stages 1, 2, and 5 are fixed-overhead infrastructure, while stages 3 and 4 carry the decisioning logic and absorb most of the model-quality investment. The connectors below show data flow; numbers on each card show the typical contribution to total latency.
What happens at each stage. What it touches, what it costs, what it can fail at.
The substantive answer to "what does Gortex actually do with my request." Each stage below has its own latency budget, its own failure modes, and its own honest current status. The technologies listed are what's actually in production today, not aspirational stack choices.
The edge layer where every request enters the system. TLS terminates at the API gateway, the API key is
validated against a hot DragonflyDB cache (with PostgreSQL fallback for cold keys), rate-limit budgets
are decremented against the partner's sliding window, and the JSON body is schema-validated against the
OpenAPI 3.1 spec. A unique request_id is minted and a trace_id is either
accepted from the inbound header or generated fresh.
This stage's job is to reject fast. Anything malformed, unauthenticated, or over-quota gets a
4xx back inside the 4ms budget without touching the decisioning hot path further down. The shape of the
response for rejection cases is identical to a successful decision shape so partner SDKs don't need
branching error paths, they get either a valid ranked array or a populated
error object in the same response envelope.
The most expensive case in this stage is a cold API key requiring a Postgres lookup, which can push the stage to 12-15ms. That's still well within the cumulative budget, but it's the only point where ingress can become a tail-latency contributor.
The data-fetch stage. The recipient identifier is hydrated into a recipient profile, every candidate ID is hydrated into a candidate feature vector, and surface-level context (time-of-day signal, locale, surface-specific metadata) is expanded into a context object. Fetches run in parallel, typically two DragonflyDB lookups for the recipient (profile + recent-decision history) plus a batch lookup for all candidate features at once.
Cache hit rate is the dominant variable here. Target hit rates are >95% on recipient profiles (hot tier, recent-active users) and >85% on candidate features (warmer set, accessed less frequently). When a miss happens, the fallback is a parameterized PostgreSQL query against the partner's writeahead-replicated feature store, which adds ~25ms but is bounded.
The output of this stage is a fully-hydrated decision context: an EnrichedRequest object
containing the recipient, the candidates with their features, the surface metadata, and the slot
configuration. From this point forward in the hot path, nothing else is fetched, stages 3 and 4 are pure
computation against the enriched context.
The decisioning core. Each enriched candidate is scored against the ranker, producing a per-candidate score with a confidence interval. The score reflects predicted relevance to the recipient at the specific surface in the specific context, the same candidate scored for a different recipient or on a different surface receives a different score.
The honest current state: the ranker is a calibrated linear model over labeled features, not a deep learned ranker. That choice is deliberate at this stage, a simple, interpretable model with hand-engineered features is what we can ship while the operations agents accumulate the judgment signal that will eventually train a learned ranker. The calibrated linear ranker is the q4 2026 target; the learned ranker follows in mid-2027 once enough operational signal has accumulated to train it.
The 35ms budget is set for a 50-candidate set. Scaling is linear in candidate count, so partner integrations that pass 200+ candidates per request will see this stage extend to ~140ms, which is why the SDK documentation recommends pre-filtering candidates at the partner side and passing at most 100 in a single request.
Where the organic ranking and the monetized slots merge into a final response. The surface's slot configuration is read (which slots are monetized, what formats they accept), the eligible bidders for each slot are identified, an auction runs to determine the winning bidder and clearing price, and the winning sponsored items are placed into their target slots with the rest of the order filled by the organic ranking from stage 3.
The honest current state: the auction logic is deterministic placement against pre-set boost configurations, not yet a real-time multi-bidder auction. A boost-paying partner gets their target slot at the configured price; there is no dynamic clearing yet. The real-time multi-bidder auction is the q1 2027 target, and is also what unlocks surge pricing as a first-class behavior rather than a flag on the boost configuration.
Reserve prices, surge multipliers, and policy-based suppression (a sponsored item cannot appear adjacent to a competing brand, a recently-shown item cannot reappear inside N decisions, etc.) all live here. These rules are themselves data, stored in the operations control plane and pushed to a hot read-replica that this stage queries, which lets the ops team adjust policy without code deploys.
The response assembly and async fanout stage. The final ordering is shaped into the response JSON, the
ranked array of IDs, the sponsored array of slot details, the
decision_id and trace_id for downstream attribution. The response is
serialized and returned to the partner over the open HTTP connection.
Simultaneously and asynchronously, a decision audit event is pushed into a local in-process buffer that batches into Kafka, a slot-billing entry is written to TigerBeetle for any sponsored placements (the ledger is the source of truth for partner financial state), and a training signal is emitted for the ranker's feedback loop. None of these block the response, if Kafka is briefly unreachable the buffer flushes when connectivity returns; if TigerBeetle is briefly unreachable the entry queues locally and writes in order when service resumes. The hot path will return 200 even if every downstream async destination is down.
This separation is what lets the data plane evolve independently of the hot path, adding new async destinations, changing the audit schema, or restructuring the training pipeline are all changes that touch only the async layer and never affect the response latency the partner sees.
The async work underneath. Where the flywheel lives.
Every hot-path decision emits events into the data plane. The data plane is what powers debugging, billing, training, and observability, none of it blocks the synchronous response, but all of it is what makes Gortex's longer-term improvement compound on itself.
The async fanout from stage 5 produces four event streams. Each one is destined for different long-term
consumers and serves a different operational purpose. The streams share the decision_id and
trace_id from the originating request, which lets the operations agents reconstruct the
full lifecycle of any decision by joining across streams.
None of these destinations are in the request's critical path. If S3 is down, the audit event sits in the Kafka log until S3 recovers. If TigerBeetle is briefly unavailable, the ledger writes queue locally. The hot path is decoupled from data-plane availability by design.
| stream | destination | what it powers | status |
|---|---|---|---|
| audit | kafka → s3 | Immutable record of every decision: inputs, outputs, model version, policy version. Powers partner-facing decision lookup, compliance answers, and any "why did Gortex return X" question after the fact. | q3 2026 |
| ledger | tigerbeetle | Double-entry book for any monetized slot. Partner balances, advertiser charges, platform revenue accrual. Reconciles against PSP settlements and feeds the billing-reconciliation agent. | q3 2026 |
| signal | kafka → s3 | Training feedback for the ranker. Outcome events (clicks, conversions, dwell) joined against the originating decision context. This is the substrate the learned ranker will train against once enough volume accumulates. | q1 2027 |
| trace | kafka → clickhouse | Per-stage span data for latency debugging and drift detection. Powers the ops team's ability to answer "why did p99 spike at 14:23 PT" without instrumentation gaps. | q3 2027 |