Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk-feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria
| TTL management | ✅ Redis + SaaS + File; Memcached refresh-only (see note) | ✅ Redis + SaaS + File + Workers (`TtlInspectable`); Memcached refresh-only (LAB-429/426) | ✅ Redis + SaaS + File (`TTLBackend`); Memcached refresh-only (LAB-430) | ❌ |
| Stale-while-revalidate (client L1) | ⚠️ L1-only mode (`backend=None`) only, and only with a TTL: the `production` and `dev` presets supply one by default from `cachekit` 0.20.0, a bare `@cache(backend=None)` still needs `ttl=`¹⁰ | ✅ Refresh-ahead within the L1 TTL: an entry past `swr_threshold_ratio` of its TTL is still served while one single-flight background refresh runs, and is never served after the TTL (LAB-728)¹⁰ ¹³ | ✅ `getWithSwr` — refresh-ahead within the L1 TTL: version tokens + background refresh, `maxConcurrentRefreshes` cap, never served after the TTL¹⁰; on Workers requires a bound `ExecutionContext` (see [Cache Backends](#cache-backends) note ¹) | ❌ |
| Stale-while-revalidate (server stale-grace) | ✅ CachekitIO backend, PyPI 0.14.0+ ([cachekit-py#228](https://github.com/cachekit-io/cachekit-py/pull/228); wheel inspected 2026-10-05): a PUT carries `X-CacheKit-Stale-TTL` from an explicit `stale_ttl=` or `@cache.io`'s default window, and a GET labelled `stale` is served at once and revalidated in the background under the lock, with a contested lock serving stale without recompute (`tests/unit/test_swr_decorator.py`). `stale_ttl` needs the CachekitIO backend known at decoration and fails there otherwise; a sub-second window is not rounded (see the SaaS API row) | ❌ | 🚧 unreleased — opt-in on `main` ([cachekit-ts#274](https://github.com/cachekit-io/cachekit-ts/pull/274)), ships in npm 0.3.0; CachekitIO backend only: `wrap()` with `staleTtl` sends `X-CacheKit-Stale-TTL` on every write of the key (cold miss, L1 refresh-ahead, revalidation), and a GET labelled `stale` is served at once and revalidated in the background, one at a time per key in each cache (a revalidation still running after 60 s no longer blocks a new one) and under the lock on a lock-capable backend, with a contested or failed lock serving stale without recompute and holding the key off for `min(lockTimeoutMs, 60 s)` (`src/cache.stale-grace.test.ts`). A sub-second window is ceiled. `wrap()` refuses `staleTtl` on an encrypted cache and on a backend without the stale-window capability. On Workers without a bound `waitUntil` a stale read takes the miss path | ❌ |
| Write-behind on a miss (return before the L2 write lands) | 🚧 unreleased — opt-in `write_behind=True` on `main` ([cachekit-py#623](https://github.com/cachekit-io/cachekit-py/pull/623)), set by no preset: the value is serialized and put in L1 on the caller's thread, and a bounded per-function writer sends the write; the async distributed-lock path and a sync miss's streaming (File + Arrow) write stay synchronous; another process's invalidation is not guaranteed to reach a queued write, which can then land after it (`decorators/write_behind.py`) | ❌ | ❌ | ❌ |
| Write-behind on a miss (return before the L2 write lands) | ✅ PyPI 0.25.0+ ([cachekit-py#623](https://github.com/cachekit-io/cachekit-py/pull/623); 0.25.0 and 0.25.1 wheels inspected 2026-11-03): opt-in `write_behind=True`, set by no preset: the value is serialized and put in L1 on the caller's thread, and a bounded per-function writer (up to 4 threads) sends each queued write; the queue holds 64 entries and 16 MiB, except that a newer value for a key whose older value is being sent always queues behind that send, with neither bound checked, so the queue can hold one entry more than 64 per send in flight (4 at most), and its bytes have no cap from those values, which stay queued while older entries are sent; an async miss that runs under the distributed lock, a timed-out lock wait included, and a sync miss's streaming (File + Arrow) write stay synchronous; another process's invalidation is not guaranteed to reach a queued write, which can then land after it; a clean interpreter exit flushes queued writes for up to 10 s (`decorators/write_behind.py`) | ❌ | ❌ | ❌ |
| Server-bounded L1 backfill (`X-CacheKit-Fresh-For`, [saas-api.md → Remaining Freshness](spec/saas-api.md#remaining-freshness)) | ⚠️ `cachekit` 0.22.0+ — the CachekitIO read honours `X-CacheKit-Freshness` and `X-CacheKit-Fresh-For`: L1 backfill is bounded to `min(ttl, Fresh-For)`, a `Fresh-For: 0` or stale read is served but never backfilled, and an absent header backfills as before; `Fresh-For` is parsed with the spec grammar (1–7 ASCII digits, at most 2,592,000, length checked first), so a sign (`+5`), `_` separators (`1_0`), whitespace, non-ASCII digits, more than seven digits (`00000005`) or a value over the cap counts as `0` (`_parse_fresh_for` in `backends/cachekitio/backend.py`, `_l1_backfill_ttl` and `_l1_backfill_from_l2` in `decorators/wrapper.py`; 0.22.0 wheel inspected 2026-10-08, `tests/unit/backends/test_cachekitio_swr_transport.py::TestFreshForRead` passes against it). 0.19.0–0.21.0 bound the backfill the same way but parse the value with Python's `int()`, so those malformed values bound it instead of counting as `0`. It never lengthens L1 past the configured TTL. The `PUT` response's `X-CacheKit-Fresh-For` is not read through 0.25.1 (wheel inspected 2026-10-31: only the read path calls `_fresh_for`), so L1's copy of the SDK's own write keeps the stated TTL, or the configured L1 TTL for a write without one, where a [plan maximum TTL](spec/saas-api.md#plan-maximum-ttl) stored a shorter one: not compliant with [API-103](spec/saas-api.md#api-103) | ⚠️ `cachekit-rs` 0.9.0+ — the CachekitIO read honours `X-CacheKit-Freshness` and `X-CacheKit-Fresh-For`: L1 backfill (and its SWR service) is bounded by `Fresh-For`, a `Fresh-For: 0` or stale read is served but never backfilled, and an absent header backfills as before (`freshness_from_headers` in `src/backend/cachekitio.rs`, `get_with_freshness` in `src/reliability.rs`; `.crate` inspected 2026-10-02, `client_tests::freshness::*` and `cachekitio::freshness_header_tests` pass against it). The `PUT` response's `X-CacheKit-Fresh-For` is not read through 0.13.1 (`.crate` inspected 2026-10-31: only `get` calls `freshness_from_headers`), so L1's copy of the SDK's own write keeps the stated TTL, or the configured L1 TTL for a write without one, where a [plan maximum TTL](spec/saas-api.md#plan-maximum-ttl) stored a shorter one: not compliant with [API-103](spec/saas-api.md#api-103) | ⚠️ `@cachekit-io/cachekit` 0.2.1+ — the CachekitIO read honours `X-CacheKit-Freshness` and `X-CacheKit-Fresh-For`: L1 backfill is bounded to `min(ttl, Fresh-For)`, on the `wrap()` SWR path too, counted from receipt; a `Fresh-For: 0` or stale read is served but never backfilled, and an absent header backfills as before; `Fresh-For` is parsed with the spec grammar (1–7 ASCII digits, at most 2,592,000), and any malformed value counts as `0` (`freshnessFromHeaders` and `getWithTtl` in `dist/backends/cachekitio.js`, the bound in `dist/cache-core.js`; `.tgz` inspected 2026-10-24; `src/backends/cachekitio-freshness.test.ts` and `test/protocol/freshness-headers.protocol.test.ts` pass at tag `cachekit-v0.2.1` against the published `cachekit-core-ts` 0.2.0 binding, run 2026-10-24). 0.1.5 reads neither header. The `PUT` response's `X-CacheKit-Fresh-For` is not read through 0.2.3 (`.tgz` inspected 2026-10-31: `set` resolves to `void`, and only the read calls `freshnessFromHeaders`), so L1's copy of the SDK's own write keeps the stated TTL, or the configured L1 TTL for a write without one, where a [plan maximum TTL](spec/saas-api.md#plan-maximum-ttl) stored a shorter one: not compliant with [API-103](spec/saas-api.md#api-103) | ❌ |

> [!IMPORTANT]
Expand Down
Loading