Patterna Docs

Timeouts and what counts in the ledger

Who gives up first, what happens to a slate nobody saw, and which slates the ledger reads.

Developers 5 min read View as Markdown

Short version: send your deadline, report every slate you gave up on, and never retry a recommendation. The ledger counts every slate Patterna served in the arm it was served in, whether or not your user saw it. The SDKs send the deadline and report abandonment for you.

The budgets#

BudgetValueWhere
SDK timeout for Recommend150 msGo, TypeScript and Python SDKs. Recommend is never retried, because every call logs an impression. Other calls retry once inside a 5 s budget.
SDK deadline sent to the engine120 msX-Deadline-Ms, inside the 150 ms timeout (RecommendDeadline in Go).
Engine serve budget120 ms by defaultSERVE_BUDGET on the engine. Past it the engine answers with the editorial list, flagged fallback: true.
Target p99, from cache60 msmeasured at the gateway
Target p99, live250 msmeasured at the gateway; see the SLO

The engine answers within its own budget or your deadline minus a tenth for the network, whichever is shorter. Because both are below the SDK's 150 ms, a slate the engine logs as served is a slate your client received.

Deadline and abandonment#

A deadline. The client sends the time it has left, in milliseconds, as the X-Deadline-Ms header or the deadline_ms body field (10 to 10,000; when both are sent, the shorter wins). The engine falls back itself rather than finishing a slate nobody will wait for.

curl
curl -sS -X POST "https://patterna-api.orven.am/v1/recommendations" \
  -H "Authorization: Bearer $PATTERNA_SERVE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Request-Id: 6f1c2a9e-3b7d-4c55-9a21-0d8e4f7b1c33" \
  -H "X-Deadline-Ms: 120" \
  -d '{"surface": "more_to_read", "mode": "non_profiled", "session_id": "s_8c1f…", "context": {"locale": "nl", "anchor_item_id": "doc_1042"}}'

Abandon. When your client gives up anyway (network trouble, a 5xx, or it hid a block it had received), it tells Patterna:

curl
curl -sS -X POST "https://patterna-api.orven.am/v1/recommendations/6f1c2a9e-3b7d-4c55-9a21-0d8e4f7b1c33/abandon" \
  -H "Authorization: Bearer $PATTERNA_SERVE_KEY"

The answer is 200 with request_id, arm, fallback: true and client_abandoned: true. The call is idempotent. The impression keeps its arm (intention to treat) and counts in the fallback rate. A client-minted UUID in X-Request-Id becomes the slate's request id, so the client knows the id even when no answer arrives. The SDKs mint it, and RecommendWithFallback reports the abandonment itself on a timeout, a network error or a 5xx (Abandon / AbandonSync for your own cases).

Do not call abandon for a slate the engine answered with shown: false. The engine has already logged it as not shown.

What counts where#

CaseArmIn the ledger?In the fallback rate?
Ranked slate, user saw ittreatment, or none without a holdoutyes, if treatmentno
Ranked slate, user never scrolled to ittreatment or noneyes, if treatment, with no tapsno
Control slate (your context.control_item_ids, the surface's fallback.by_anchor list for the anchor, or the surface's list)controlyesno: control is not a fallback
Engine fallback (past its budget, storage trouble, no active flow)the assigned armyes, in that armyes
Client gave up and reported abandonthe assigned armyes, with no tapsyes (client_abandoned)
Client gave up and reported nothingthe assigned armyes, with no tapsno: this is an under-count, so report it
Client hid the block after hydration and reported abandonthe assigned armyes, with no tapsyes (client_abandoned)
Slate shorter than the surface's min_slots, answered with shown: false and no itemsthe assigned armyes, with no success, in both arms (intention to treat)no
Request refused (4xx)none: no impressionnono
Surface in shadow, or "shadow": truenonenono
Surface in testersnonenoyes, if flagged
No running holdoutnonenoyes, if flagged
Outside the experiment's windowas loggednoyes, if flagged
context.measured: falsethe assigned armno, in either armyes, if flagged
Synthetic example dataas generatednevernever

One definition: the fallback rate is fallback slates (the engine's safety-net fallbacks plus slates the client abandoned) over non-shadow slates served. The overview's 7-day figure counts every non-shadow slate; a surface's page leaves shown: false slates out of both counts, because they carried no items. Next to it the surface metrics report client_abandoned_rate and unmeasured_share (slates requested with context.measured: false).

A shown: false slate counts in its arm (since engine 0.3; engine 0.2 left such slates out, and because control's list is always padded to full length and treatment's could fall short, that exclusion depended on the arm). min_slots is counted before the arm, from the items that pass the request's rules, so both arms hide alike. A treatment slate that comes out short is filled from the editorial list, the same list control uses; the answer and the impression say how many positions were filled in fill_count, and each filled slot in the logged slate (trace, export) carries fill: true with propensity 1.

Why every served slate counts#

Intention to treat means: once a unit is assigned to an arm, every slate served to it counts in that arm, whatever happened next. The alternative, counting only slates that were seen, sounds cleaner but is biased. If Patterna's ranking is slower than the editorial list, more treatment slates time out, and dropping them removes exactly the cases where treatment did worse. The ledger would then flatter the slower arm. Counting them as served with no taps charges the cost to the arm that caused it. The same holds for a block hidden because it had too few items: whether it is hidden by the engine (shown: false) or by your app (abandon), it stays in its arm.

Unmeasured slates are different: whether a user allowed analytics is decided before the arm can have any effect, and it is decided the same way in both arms. Leaving them out keeps the comparison fair and makes the rates mean something.

What to do in your client#

  1. Use RecommendWithFallback (or the same pattern): your own list on a timeout, 5xx, 429 or an open breaker.
  2. Never retry Recommend. A retry logs a second impression for what the user sees as one list, and the first one counts as served with no taps.
  3. With the SDKs: nothing more. They send the deadline and report abandonment. With your own client: mint X-Request-Id, send X-Deadline-Ms (or deadline_ms), and call abandon when you give up.
  4. Report seen and taps with the slate's request_id and the served position (from 0), for both arms the same way. The engine takes kind and position from the logged slate, refuses items that were not in it (422 unknown_item) and accepts an optional block name when one slate is shown in two blocks (the ledger can then give a row per block with by=block).
  5. If the user did not allow usage analytics and you will not report, send context.measured: false.
  6. Set min_slots on the surface (PATCH /surfaces/{key}) to the smallest list worth showing, usually 1, and hide the block when the answer says shown: false. Never call abandon for such a slate. If you hide a block for another reason (hydration dropped items), report it with abandon.
esc
  • Type to search every page, heading and code sample.