Skip to content
33 changes: 33 additions & 0 deletions changelog.d/20261031_lab-12077.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### SaaS API — L1 hit-latency telemetry and the telemetry switch

- [`spec/saas-api.md` § L1 hit latency](spec/saas-api.md#l1-hit-latency) (new) defines three
optional headers that report how long calls served from L1 take, as the caller sees them:
`X-CacheKit-L1-Latency-Buckets` (sixteen fixed buckets doubling from 128 ns to about
2.1 ms, the last one up to 60 s; their sum is the number of hits timed),
`X-CacheKit-L1-Latency-Sum-Ns`, and `X-CacheKit-L1-Latency-One-In` (the sampling
denominator N, so readers weight each report). Values are counted per report, not per
session, kept per CachekitIO backend instance, taken atomically and never re-sent once an
attempt may have been delivered. An SDK may time a random sample of calls, and sends no
latency headers from a runtime whose clock cannot resolve an L1 hit, such as Cloudflare
Workers ([API-79](spec/saas-api.md#api-79)–[API-87](spec/saas-api.md#api-87)).
- [`spec/saas-api.md` § Server handling](spec/saas-api.md#server-handling) (new): the server
validates the latency and session headers as independent sets, never rejects a request over
one, drops valid latency sets only at random and records each kept set's probability, weights
tenants equally (or caps them) in cross-tenant aggregates, and never lets an optional metrics
header change billing or a rate-limit verdict
([API-88](spec/saas-api.md#api-88)–[API-93](spec/saas-api.md#api-93)).
- [`spec/saas-api.md` § Telemetry switch](spec/saas-api.md#telemetry-switch) (new): every SDK
exposes an optional `telemetry` setting and reads `CACHEKIT_TELEMETRY`, once, at backend
construction; either one turned off stops every optional metrics header. `X-CacheKit-L1-Status`
and any `User-Agent` continue. Default: on
([API-94](spec/saas-api.md#api-94)–[API-96](spec/saas-api.md#api-96)).
- [`spec/saas-api.md` § Optional Metrics Headers](spec/saas-api.md#optional-metrics-headers):
the headers are client-reported observability, sent only to the configured CachekitIO API
([API-77](spec/saas-api.md#api-77)).
- [`spec/saas-api.md` § L1 hit rate](spec/saas-api.md#l1-hit-rate) (new) pins
`X-CacheKit-L1-Hit-Rate` to `L1-Hits / (L1-Hits + L2-Hits + Misses)`
([API-78](spec/saas-api.md#api-78)). **Changes cachekit-py**, which sends
`L1-Hits / (L1-Hits + L2-Hits)`.
- [`sdk-feature-matrix.md` § Observability](sdk-feature-matrix.md#observability): rows for the
latency headers and the telemetry switch, unshipped in every SDK, and the Python hit-rate
difference on the session-headers row.
28 changes: 24 additions & 4 deletions conformance/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ How ids are assigned, and what each status means: [README.md](README.md#coverage
| [`spec/file-backend-format.md`](../spec/file-backend-format.md) | 6 | 1 | 3 | 0 | 2 |
| [`spec/intent-presets.md`](../spec/intent-presets.md) | 55 | 0 | 19 | 0 | 36 |
| [`spec/interop-mode.md`](../spec/interop-mode.md) | 36 | 5 | 23 | 4 | 4 |
| [`spec/saas-api.md`](../spec/saas-api.md) | 76 | 0 | 15 | 0 | 61 |
| [`spec/saas-api.md`](../spec/saas-api.md) | 96 | 0 | 15 | 0 | 81 |
| [`spec/wire-format.md`](../spec/wire-format.md) | 24 | 3 | 18 | 3 | 0 |

| SDK | Covered | Partial | Uncovered | Gap | n/a |
| :--- | ---: | ---: | ---: | ---: | ---: |
| cachekit-py | 12 | 86 | 5 | 69 | 47 |
| cachekit-ts | 9 | 78 | 7 | 59 | 66 |
| cachekit-rs | 4 | 69 | 3 | 55 | 88 |
| cachekit-py | 12 | 86 | 5 | 83 | 53 |
| cachekit-ts | 9 | 78 | 7 | 73 | 72 |
| cachekit-rs | 4 | 69 | 3 | 69 | 94 |

Not indexed yet: [`spec/interop-v2.md`](../spec/interop-v2.md).

Expand Down Expand Up @@ -274,6 +274,26 @@ Marked not-a-requirement in `spec/intent-presets.md`:
| [API-74](../spec/saas-api.md#api-74) | *SSRF Protection*: All SDKs **MUST** enforce SSRF protection when accepting the API URL. The following rules apply: | **Gap:** No vector holds an API URL, so nothing here checks HTTPS-only, the listed private ranges or the hostname allowlist. | gap | gap | gap |
| [API-75](../spec/saas-api.md#api-75) | *Counting outcomes*: Plain read. A value read **MUST** count one outcome: an L1 hit when the in-memory layer served it, an L2 hit when a backend read did, or a miss when … | **Gap:** No vector covers request headers, so nothing here catches a client that counts a plain read twice, or counts a read that failed at the backend; only the SDK's own counter tests do. | gap | gap | gap |
| [API-76](../spec/saas-api.md#api-76) | *Counting outcomes*: … caching wrapper (the cachekit-py decorators, cachekit-ts `wrap()`, cachekit-rs `#[cachekit]`) **MUST** count one outcome, however many reads it makes. A call that runs the wrapped function counts one miss, before its … | **Gap:** No vector covers request headers, so nothing here catches a client that counts a wrapped call that runs the function as anything but one miss, a joined in-process caller as anything but one L1 hit, or a miss on each single-flight re-poll or lock re-check; only the SDK's own counter tests do. | gap | gap | gap |
| [API-77](../spec/saas-api.md#api-77) | *Optional Metrics Headers*: … them only while telemetry is on (Telemetry switch), and only from its CachekitIO backend: an SDK **MUST NOT** send them, or a report built for them, to any host but the CachekitIO API that backend is configured with. A Redis, … | **Gap:** No vector covers request headers, so nothing here catches a client that sends the optional metrics headers, or a report built for them, through a Redis, Memcached or File backend or to any host but its configured CachekitIO API. | gap | gap | gap |
| [API-78](../spec/saas-api.md#api-78) | *L1 hit rate*: An SDK **MUST** compute `X-CacheKit-L1-Hit-Rate` as `L1-Hits / (L1-Hits + L2-Hits + Misses)` from the counter values on the same … | **Gap:** No vector covers request headers, so nothing here catches a client that computes X-CacheKit-L1-Hit-Rate as L1-Hits / (L1-Hits + L2-Hits), sends it with other than three decimal places, or sends other than 0.000 for all-zero counters. | gap | gap | gap |
| [API-79](../spec/saas-api.md#api-79) | *L1 hit latency*: … start of the wrapped function. Only work that runs after the hand-back is outside it. An SDK **MUST** add to a report only the durations of calls it counts as an L1 hit and that return a value. It MUST NOT add the … | **Gap:** No vector reaches an SDK's timing, so nothing here catches a client that adds L2 hits, misses or failed calls to the latency report; only the SDK's own tests do. | gap | gap | gap |
| [API-80](../spec/saas-api.md#api-80) | *L1 hit latency*: … add to a report only the durations of calls it counts as an L1 hit and that return a value. It **MUST NOT** add the duration of any of these: | **Gap:** As API-79, for a client that adds a joined in-process call, an existence check or a read made inside a wrapped call. | gap | gap | gap |
| [API-81](../spec/saas-api.md#api-81) | *L1 hit latency*: … rather than every call, so that two clock reads do not add measurably to a sub-microsecond hit. It **MUST** decide whether to time a call before it knows the call's outcome, by an independent pseudo-random draw for each call … | **Gap:** As API-79, for a client that decides to time a call after it knows the outcome, samples by a counter, lets the key, arguments, position, outcome or duration sway the draw, or draws with a probability other than the 1/N it sends. | gap | gap | gap |
| [API-82](../spec/saas-api.md#api-82) | *L1 hit latency*: Clock. An SDK **MUST NOT** send the latency headers unless it timed with a monotonic clock that advances during synchronous execution and … | **Gap:** No vector covers request headers, so nothing here catches a client that sends latency headers timed with a clock that does not advance during synchronous execution or resolves coarser than 100 ns, such as on Cloudflare Workers. | gap | gap | gap |
| [API-83](../spec/saas-api.md#api-83) | *L1 hit latency*: … that backend's requests, so one backend's hits never ride another's requests. A process that forks **MUST** start the child with an empty accumulator, so that the parent's hits are not reported twice. | **Gap:** No vector reaches a forked process, so nothing here catches a child that inherits and re-sends its parent's accumulated report. | gap | gap | gap |
| [API-84](../spec/saas-api.md#api-84) | *L1 hit latency*: … sum test is part of the atomic step below, so a racing request never attaches all-zero buckets. It **MUST NOT** attach a report to a `GET /v1/cache/health` request, which the server does not record. When an SDK attaches a report … | **Gap:** No vector covers request headers, so nothing here catches a client that attaches a latency report to a health request. | gap | gap | gap |
| [API-85](../spec/saas-api.md#api-85) | *L1 hit latency*: … request, which the server does not record. When an SDK attaches a report to a request, it **MUST** take every value and reset the accumulator in one atomic step, so that no two attempts carry the same timed hit and … | **Gap:** No vector covers concurrent requests, so nothing here catches a client whose concurrent attempts carry the same timed hit, whose buckets and sum describe different hits, or that sends cumulative rather than per-report values. | gap | gap | gap |
| [API-86](../spec/saas-api.md#api-86) | *L1 hit latency*: … that carries it may have reached the server, the report is consumed whatever the outcome: the SDK **MUST NOT** add its values back, or send them on any later attempt, including a retry its HTTP library makes on its own. Only an … | **Gap:** No vector covers request headers, so nothing here catches a client that adds a consumed report back to its accumulator, or whose own or its HTTP library's retries resend it after an attempt that may have been delivered. | gap | gap | gap |
| [API-87](../spec/saas-api.md#api-87) | *L1 hit latency*: … (2^53 − 1, the largest integer a JavaScript number or an IEEE 754 double holds exactly). An SDK **MUST** send values that meet this grammar and these rules: | **Gap:** No vector covers request headers, so nothing here catches a client that sends a latency value outside the integer grammar, other than sixteen buckets, all-zero buckets, a sum of 0, a one-in value outside 1 to 1,000,000, a duration in the wrong bucket, or a duration above 60 s. | gap | gap | gap |
| [API-88](../spec/saas-api.md#api-88) | *Server handling*: The server validates the latency headers as one set and the session headers as another. It **MUST NOT** drop either set because the other is missing, malformed or rejected: a latency set is recorded when the session … | Binds the server.<br>**Gap:** No public vector exercises the server, so nothing here catches one that drops the session headers because a latency header is malformed, or the latency headers because the session headers are missing, malformed or rejected. | n/a | n/a | n/a |
| [API-89](../spec/saas-api.md#api-89) | *Server handling*: … headers fail any of the server's checks, and the reverse. Within the latency set, the server **MUST** drop all three latency values when any of these holds: a header is missing or malformed; the sum is 0; the One-In … | Binds the server.<br>**Gap:** No public vector exercises the server, so nothing here catches one that records a latency set with a missing or malformed header, a sum of 0, a One-In outside 1 to 1,000,000, or a sum its buckets cannot produce. | n/a | n/a | n/a |
| [API-90](../spec/saas-api.md#api-90) | *Server handling*: … most the sum of each count times the bucket's bound (60,000,000,000 ns for bucket 16). The server **MUST NOT** answer a request with an error because an optional metrics header is missing or malformed, and SHOULD record a valid … | Binds the server.<br>**Gap:** As API-88, for a server that answers a request with an error because an optional metrics header is missing or malformed. | n/a | n/a | n/a |
| [API-91](../spec/saas-api.md#api-91) | *Server handling*: … its response status, except `GET /v1/cache/health`. A server that records valid latency sets **MUST** keep or drop each one only by a random draw, whose probability may vary by tenant, time or load, for example to stay … | Binds the server.<br>**Gap:** No public vector exercises the server, so nothing here catches one that drops latency sets deterministically, such as once a write budget is spent, or keeps them without recording the probability it kept each one. | n/a | n/a | n/a |
| [API-92](../spec/saas-api.md#api-92) | *Server handling*: … every other client together. An aggregate of latency sets across tenants, or one shown publicly, **MUST** weight each tenant equally or cap each tenant's share, and never sums raw sets across tenants. | Binds the server.<br>**Gap:** How a server's cross-tenant or public latency aggregate weights tenants is server code no public vector or SDK test can observe. | n/a | n/a | n/a |
| [API-93](../spec/saas-api.md#api-93) | *Server handling*: The server **MUST NOT** let an optional metrics header change billing or rate limiting, or let the value of `X-CacheKit-L1-Status` change … | Binds the server.<br>**Gap:** Which inputs a server's billing and rate-limit verdicts read, including whether billing reads X-CacheKit-L1-Status, is server code no public vector or SDK test can observe. | n/a | n/a | n/a |
| [API-94](../spec/saas-api.md#api-94) | *Telemetry switch*: Every SDK **MUST** let its user turn telemetry off through both of these controls, which mean the same in every SDK: | **Gap:** No vector reaches an SDK's configuration, so nothing here catches a client that lacks the telemetry setting, defaults it to true or false, or ignores CACHEKIT_TELEMETRY. | gap | gap | gap |
| [API-95](../spec/saas-api.md#api-95) | *Telemetry switch*: An SDK **MUST** read both controls once, when it constructs the backend, and resolve them exactly as below, for that backend … | **Gap:** As API-94, for a client that lets a configuration loader fill the setting from the environment, lets on win over off, reads an unrecognised value as on, or re-reads the controls after construction. | gap | gap | gap |
| [API-96](../spec/saas-api.md#api-96) | *Telemetry switch*: With telemetry off, an SDK **MUST NOT** send any optional metrics header on that backend's requests: `X-CacheKit-Session-ID`, `X-CacheKit-Session-Start`, … | **Gap:** No vector covers request headers, so nothing here catches a client that, with telemetry off, still sends a session or latency header. | gap | gap | gap |

Marked not-a-requirement in `spec/saas-api.md`:

Expand Down
Loading
Loading