anatomy // what happens inside

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.

# 01 / the hot path

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.

01
ingress
tls termination, auth, rate-limit, schema validation, trace mint
02
enrich
recipient profile fetch, candidate feature lookup, context expansion
03
rank
score enriched candidates against the ranker; emit per-candidate scores with confidence
04
resolve
run slot auction, apply reserve and surge, merge sponsored placements with organic ranking
05
emit
assemble response, fire async fanout to kafka and ledger, return 200
cumulative typical
~100ms
p99 budget
< 200ms
headroom
~100ms
The latency budgets shown are conservative. Stage 2 (enrich) is the largest line item because it owns all the data-fetch I/O, cache hits absorb most of it, but the worst-case profile of a cold candidate against a Postgres fallback is what sets the upper bound. Stage 3 (rank) carries the highest variance because it scales with candidate-set size; the ~35ms target assumes 50 candidates. Stage 5 (emit) explicitly does not wait for Kafka or the ledger to acknowledge, those are fire-and-forget into a local agent buffer that flushes asynchronously, which is what keeps emit consistently sub-10ms.
# 02 / stage detail

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.

# 01 / ingress ingress ~4ms budget q3 2026

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.

tech
api gateway dragonflydb postgresql openapi 3.1
# 02 / enrich enrich ~40ms budget q3 2026

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.

tech
dragonflydb postgresql / aurora parallel reads
# 03 / rank rank ~35ms budget q4 2026

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.

tech
calibrated linear (today) learned ranker (building) in-memory feature store
# 04 / resolve resolve ~15ms budget q1 2027

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.

tech
deterministic placement (today) real-time auction (building) policy hot-replica
# 05 / emit emit ~6ms budget q3 2026

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.

tech
in-process buffer kafka / redpanda tigerbeetle s3
# 03 / data plane

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.

async fanout

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
The audit and ledger streams are load-bearing from day one, they're what make the platform commercially operable and partner-trustable, which is why both ship live before anything else. The signal stream is the substrate for the operations-to-decisioning flywheel: every ops-agent judgment and every observed outcome compounds into training data that improves the ranker over time. The trace stream is observability, important but deferred until the platform has enough partners to justify the ClickHouse operational cost.