Skip to content

Diagnostic LLM APIs

Cache hits fall away exactly when the fleet scales out

The cached share was fine in staging and fine in the first week of the rollout. Then autoscaling started doing its job, and the discount went the wrong way. At three in the morning the prompt caches at seventy per cent; at two in the afternoon, on the same template, the same model and the same code, it caches at sixteen. Everybody's first instinct is that something about the busy path is different. Nothing about the busy path is different. There are simply more machines in it, and none of them has seen this prefix before.

Read-only key Python and Node.js Tests included
Server rack with blinking green lights
Photo by Domaintechnik on Unsplash
The short answer

With an organization admin key: GET /v1/organization/usage/completions?start_time={T-7d}&bucket_width=1h&limit=168&group_by[]=project_id&group_by[]=model. Per hour, input_cached_tokens / input_tokens against num_model_requests.

Then correlate the two. A rank correlation at or below about -0.4, with the quiet third of hours caching well and the busy third caching at well under half that, is routing scatter. Prefix instability does not produce a slope; it produces a flat low line at every load.

Drop the hours that follow a gap in traffic before correlating. Those start cold no matter how the requests were routed, and if the batch that opens a burst is also its busiest hour — which is normal — leaving them in manufactures precisely the negative correlation you are testing for. Those hours belong to the retention note.

The repair is one parameter: prompt_cache_key, set to something coarse and stable per template. It influences routing. It does not pin a request to a machine and does not guarantee a hit.

The problem in plain words

OpenAI's prompt caching is automatic, which is usually described as a convenience and is occasionally a problem: there is no flag to switch on, so there is no flag whose absence explains a miss. Lookup is prefix-based, and it is also routing-sensitive. A cached prefix lives on the machine that served the request that created it. Send the next identical request to a different backend and it is a first call again, at full price, and nothing tells you that is what happened.

The consequence is genuinely counter-intuitive, which is why it survives so long. Every other cache in your stack gets more effective as traffic rises, because volume is what keeps entries warm. This one gets less effective, because volume is what spreads the traffic over more caches. A team scaling a service horizontally watches its per-request cost climb and reaches for the explanations that usually fit — a longer prompt, more retrieved context, a model change — and none of them fit, because the prompt is byte-identical and always was.

One template,one prefixidentical on everycallAutoscaler addsworkersthe fleet getswiderEach backend iscoldno worker sees ittwicePeak hourcaches worstthe discountinvertsScaling makesit worsewhich nobodyexpects
Nothing on the busy path is different. There are simply more machines in it, and the prefix is new to every one of them.

Why it happens

The slope is the finding, not the level. Three published notes and one sibling all read a cached share, so a low share is not evidence of anything by itself. What no other note here computes is the relationship between that share and the hour's request count. A rank correlation is used rather than a straight one because request counts are heavy tailed: one incident hour at ten times normal volume would otherwise decide the answer on its own.

Post-gap hours are excluded, and that exclusion is the whole boundary with the retention note. An hour that resumes traffic after idle time starts from an evicted cache regardless of routing, so it can say nothing about routing. Worse, scheduled work usually opens at full tilt, which makes the coldest hour also the busiest one and forges a strong negative correlation out of an eviction problem. The script computes the correlation over linked hours only, reports how many hours it threw away, and names the note those hours belong to.

Two degenerate cases get two different answers on purpose. A request rate that never varies returns no correlation at all, because nothing can be said about concurrency from a flat load. A cached share that never varies returns exactly zero, because "no relationship" is itself a finding and it is the one that points at a prefix changing between calls. Collapsing those two into one number would hide the difference between "cannot tell" and "definitely not this".

A share that rises with load is reported and is not a fault. Density keeps entries warm, so on a well-keyed fleet the busy hours should cache better than the quiet ones. Seeing that is the confirmation that the prefix is stable and the routing hint is doing its job, and the script prints it rather than staying silent, because a check that only ever speaks up when it is unhappy teaches you nothing about the healthy shape.

The quiet hours have to actually cache before any of this means anything. The finding requires a quiet-third share above a floor. If the prompt caches badly even at four in the morning with two workers running, the problem is not where the requests landed, and no routing hint will fix it. That branch is handed to the prefix note and to the eligibility check on the mean input size.

The fix, as a flow

This is the only cache fault in the section that gets worse when you add capacity, which is why it survives so long: every instinct says volume keeps a cache warm. Here volume spreads the same prompt over more machines, none of which has seen it before. The evidence is a slope rather than a level, and the hours that follow a gap are thrown away first, because those run cold whatever the routing did.

The hourly cached shareagainst the request rateFalls as the load risesrequests miss a cache that is warmFlat at every loadthe prefix moves between callsCold only after gapsthat is the retention noteClimbs with the loaddensity is keeping entries warm
The level is shared with three other notes. The response to load is not, and it is the only thing here that points at routing.

How to fix it

Pull a week of hourly buckets with the request count

bucket_width=1h with group_by[]=project_id and group_by[]=model. num_model_requests is the field that makes this note possible and it is the one Anthropic's equivalent report does not have, which is why this check is an OpenAI check.

Drop every hour that follows a gap

Keep only hours whose preceding hour also carried traffic. This is not tidying: post-gap hours run cold for a reason that has nothing to do with concurrency, and they cluster at the start of scheduled work, which is exactly when load spikes.

Pool the share, do not average it

Cached tokens over input tokens across the set, not the mean of the per-hour ratios. An hour with nine requests must not outvote an hour with nine thousand, and the whole quantity under test is what happens in the big hours.

Rank-correlate the share against the request rate

Spearman across the linked hours. At or below about -0.4, with the quiet third caching above a floor and the busy third at under sixty per cent of it, the share is degrading with load. Report both the correlation and the two shares: the number that persuades anyone is "seventy per cent at night, sixteen at noon".

Set a coarse cache key and re-measure the busy end

prompt_cache_key="rag-answer-v3", or the template name plus a tenant. Coarse enough that traffic concentrates; a per-request id scatters the fleet as thoroughly as no key at all. It is a routing hint, not content, so keep it out of the prompt. What moves first afterwards is the gap between the quiet and busy shares, not the average.

How to check it worked

Re-run over the same seven days after the key ships. The quiet share should barely move and the busy share should climb toward it; the correlation is the number to watch, because it goes to roughly zero long before the average looks impressive.

python3 openai_cache_key_routing_scatter.py --days 7
# load-correlated-misses   proj_abc123 / gpt-5.6  cached share 68% in the quietest hours (187 req/h) and 16% in the busiest (3405 req/h), rank correlation -1.00 against request rate. The prefix is cacheable; the requests are not landing where it is cached.
#   21 hour(s) that follow a gap in traffic were excluded before correlating
#   repair: set prompt_cache_key on the route
#   repair: make it coarse. The template name, or the template plus tenant
# 5 project/model series checked, 1 finding(s)

The full code

One paginated GET and no writes. Ten pure functions, and the two that carry the note are the exclusion and the correlation. continuation_rows keeps only hours whose predecessor also had traffic, which is what stops an eviction problem being read as a routing one; resumption_rows is its complement, kept so the count of discarded hours can be printed rather than quietly vanishing. spearman ranks with ties shared and returns two different degenerate answers on purpose: nothing at all when the load is flat, and exactly zero when the share is. load_split turns the slope into the pair of numbers anyone will actually quote.

Get this script on GitHub Follow @allanninal Python and Node.js, with tests. Read only, it never writes. One of 97 LLM API fixes, free and open source.
openai_cache_key_routing_scatter.py
"""Find OpenAI traffic whose cached share falls as concurrency rises.

Read only. One paginated GET against the Usage API, which needs an admin key
(sk-admin-...); a project key is rejected by every /v1/organization/ path, and
an admin key can be provisioned with the read scopes only.

Cache lookup is prefix-based and routing-sensitive. Without prompt_cache_key a
fleet sprays byte-identical prompts across many backends and each one sees a
cold prefix, so the hit rate gets *worse* as you scale out. That is the whole
signature: a cached share that is negatively correlated with the hour's request
count. A prefix that is simply unstable produces a flat low share at every load
and belongs to a different note.

Hours that follow a gap in traffic are dropped before anything is correlated.
Those hours run cold because the entry was evicted while nobody was calling,
which is a third note again, and leaving them in would manufacture exactly the
negative correlation this one is looking for.

The repair is printed, never performed.
"""
import argparse
import datetime as dt
import logging
import os
import sys

import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("openai_cache_key_routing_scatter")

API = "https://api.openai.com/v1"

FINDINGS = ("load-correlated-misses",)


def _int(value):
    """Read a usage field as an int. Pure. Missing and unreadable both mean 0."""
    try:
        return int(value or 0)
    except (TypeError, ValueError):
        return 0


def hour_index(stamp):
    """Hours since the epoch. Pure. None if unreadable.

    The usage buckets carry start_time as a unix integer, but the same code has
    to survive an ISO string, and adjacency has to be integer arithmetic: gap
    detection on formatted timestamps gets 23:00 and 00:00 wrong every night.
    """
    if isinstance(stamp, bool) or stamp is None:
        return None
    if isinstance(stamp, (int, float)):
        return int(stamp) // 3600
    text = str(stamp).strip().replace(" ", "T")
    if len(text) < 13:
        return None
    head = text[:13]
    if head[4] != "-" or head[7] != "-" or head[10] != "T":
        return None
    for part in (head[0:4], head[5:7], head[8:10], head[11:13]):
        if not part.isdigit():
            return None
    try:
        when = dt.datetime(int(head[0:4]), int(head[5:7]), int(head[8:10]),
                           int(head[11:13]), tzinfo=dt.timezone.utc)
    except ValueError:
        return None
    return int(when.timestamp()) // 3600


def hour_label(index):
    """Render an hour index back as a UTC stamp. Pure."""
    if index is None:
        return "unknown"
    when = dt.datetime.fromtimestamp(int(index) * 3600, dt.timezone.utc)
    return when.strftime("%Y-%m-%dT%H:00Z")


def rows_by_series(buckets):
    """Per (project_id, model), one row per active hour, sorted. Pure."""
    merged = {}
    for bucket in buckets or []:
        index = hour_index(bucket.get("start_time"))
        if index is None:
            continue
        for result in bucket.get("results") or []:
            if not isinstance(result, dict):
                continue
            ident = (str(result.get("project_id") or "unknown"),
                     str(result.get("model") or "unknown"))
            row = merged.setdefault((ident, index),
                                    {"index": index, "hour": hour_label(index),
                                     "requests": 0, "input": 0, "cached": 0})
            row["requests"] += _int(result.get("num_model_requests"))
            row["input"] += _int(result.get("input_tokens"))
            row["cached"] += _int(result.get("input_cached_tokens"))
    out = {}
    for (ident, _index), row in merged.items():
        if row["requests"] > 0 or row["input"] > 0:
            out.setdefault(ident, []).append(row)
    for rows in out.values():
        rows.sort(key=lambda r: r["index"])
    return out


def cached_share(rows):
    """Pooled cached share over a set of hours. Pure. None when nothing ran.

    Pooled rather than averaged: an hour with nine requests must not carry the
    same weight as an hour with nine thousand, which is the entire quantity
    under test here.
    """
    total = sum(_int(r.get("input")) for r in rows or [])
    if total <= 0:
        return None
    return sum(_int(r.get("cached")) for r in rows or []) / float(total)


def continuation_rows(rows):
    """Hours whose previous hour also carried traffic. Pure.

    The exclusion that keeps this note off someone else's ground. An hour that
    follows idle time starts from an evicted cache no matter how the requests
    were routed, so it cannot be evidence about routing.
    """
    active = {_int(r.get("index")) for r in rows or []}
    return [r for r in rows or [] if _int(r.get("index")) - 1 in active]


def resumption_rows(rows):
    """The hours the correlation deliberately threw away. Pure."""
    active = {_int(r.get("index")) for r in rows or []}
    return [r for r in rows or [] if _int(r.get("index")) - 1 not in active]


def _ranks(values):
    """Average ranks, ties shared. Pure."""
    order = sorted(range(len(values)), key=lambda i: values[i])
    ranks = [0.0] * len(values)
    i = 0
    while i < len(order):
        j = i
        while j + 1 < len(order) and values[order[j + 1]] == values[order[i]]:
            j += 1
        shared = (i + j) / 2.0 + 1.0
        for k in range(i, j + 1):
            ranks[order[k]] = shared
        i = j + 1
    return ranks


def spearman(xs, ys):
    """Rank correlation between two equal-length series. Pure. None if flat.

    Rank rather than Pearson because request counts are heavy tailed and one
    incident hour would otherwise decide the answer.

    The two degenerate cases are deliberately different answers. A load that
    never varies returns None, because nothing at all can be said about
    concurrency from a flat request rate. A share that never varies returns
    0.0, because "no relationship" is a real finding here and it is the one
    that sends the reader to the prefix-instability note.
    """
    xs = list(xs or [])
    ys = list(ys or [])
    if len(xs) != len(ys) or len(xs) < 8:
        return None
    rx, ry = _ranks(xs), _ranks(ys)
    n = float(len(xs))
    mx, my = sum(rx) / n, sum(ry) / n
    sxy = sum((a - mx) * (b - my) for a, b in zip(rx, ry))
    sxx = sum((a - mx) ** 2 for a in rx)
    syy = sum((b - my) ** 2 for b in ry)
    if sxx <= 0:
        return None
    if syy <= 0:
        return 0.0
    return sxy / ((sxx ** 0.5) * (syy ** 0.5))


def load_split(rows, fraction=0.33):
    """Pooled cached share in the quietest and busiest hours. Pure.

    Returns (quiet_share, busy_share, quiet_rate, busy_rate) or Nones. The
    correlation says the relationship is monotone; this says how big it is in
    the only units anyone will act on, which is the discount you are not getting
    at peak.
    """
    active = [r for r in rows or [] if _int(r.get("requests")) > 0]
    if len(active) < 6:
        return (None, None, None, None)
    ordered = sorted(active, key=lambda r: _int(r.get("requests")))
    size = max(2, int(len(ordered) * fraction))
    quiet, busy = ordered[:size], ordered[-size:]

    def rate(part):
        return sum(_int(r.get("requests")) for r in part) / float(len(part))

    return (cached_share(quiet), cached_share(busy), rate(quiet), rate(busy))


def handoff(state):
    """Which note owns this shape, when it is not this one. Pure."""
    if state == "no-cached-tokens":
        return ("not one cached token at any load, so the traffic never becomes "
                "eligible rather than being routed away from its cache. Read the "
                "prompt-below-model-cache-minimum note and check the mean input "
                "per request against the model's floor first.")
    if state == "flat-low-share":
        return ("the share is low and stays low whatever the load, which is a "
                "prefix that differs between calls rather than requests landing "
                "on different machines. Read the "
                "cache-invalidated-by-changing-prefix note.")
    if state == "cold-only-after-idle":
        return ("the cold hours are the ones that follow gaps in traffic, and "
                "the busy hours are fine. That is eviction during idle time: "
                "read the prompt-cache-retention-left-at-default note.")
    return ""


def classify(rows, rho_floor=-0.4, ratio_floor=0.6, quiet_floor=0.15,
             min_hours=24):
    """Classify one project and model series. Pure. Returns (state, detail).

    Only a series whose cached share falls monotonically as the hour gets busier
    belongs to this note, and only after the post-gap hours have been removed.
    """
    rows = rows or []
    linked = continuation_rows(rows)
    if len(linked) < min_hours:
        return ("too-few-linked-hours",
                "%d hour(s) with traffic in the hour before them, under the "
                "floor of %d. Correlating against load needs a run of busy "
                "hours." % (len(linked), min_hours))

    overall = cached_share(linked)
    if overall is not None and overall <= 0.0:
        return ("no-cached-tokens",
                "%d input token(s) across %d linked hour(s) and not one cached"
                % (sum(_int(r.get("input")) for r in linked), len(linked)))

    quiet, busy, quiet_rate, busy_rate = load_split(linked)
    rho = spearman([_int(r.get("requests")) for r in linked],
                   [cached_share([r]) or 0.0 for r in linked])

    if rho is None or quiet is None or busy is None:
        return ("load-does-not-vary",
                "the request rate barely moves across the window, so nothing "
                "here can be attributed to concurrency")

    if rho <= rho_floor and quiet >= quiet_floor and busy <= quiet * ratio_floor:
        return ("load-correlated-misses",
                "cached share %.0f%% in the quietest hours (%.0f req/h) and "
                "%.0f%% in the busiest (%.0f req/h), rank correlation %.2f "
                "against request rate. The prefix is cacheable; the requests "
                "are not landing where it is cached."
                % (quiet * 100, quiet_rate, busy * 100, busy_rate, rho))

    if rho >= -rho_floor:
        return ("share-rises-with-load",
                "cached share climbs with the request rate (%.2f): density is "
                "keeping entries warm, which is the opposite of scatter" % rho)

    cold = resumption_rows(rows)
    cold_share = cached_share(cold)
    if (overall is not None and cold_share is not None and cold_share <= 0.02
            and overall >= quiet_floor and len(cold) >= 3):
        return ("cold-only-after-idle",
                "%.0f%% cached in linked hours against %.0f%% in the %d hour(s) "
                "that follow a gap" % (overall * 100, cold_share * 100, len(cold)))

    if overall is not None and overall < quiet_floor:
        return ("flat-low-share",
                "cached share %.0f%% overall with rank correlation %.2f against "
                "load: low everywhere rather than low under load"
                % (overall * 100, rho))

    return ("healthy",
            "cached share %.0f%% quiet and %.0f%% busy, correlation %.2f"
            % (quiet * 100, busy * 100, rho))


def repair_lines():
    """The routing hint, and what makes a good one. Pure."""
    return [
        "set prompt_cache_key on the route: "
        "client.responses.create(..., prompt_cache_key=\"rag-answer-v3\").",
        "make it coarse. The template name, or the template plus tenant, so "
        "traffic concentrates on a few caches. A per-request id scatters the "
        "fleet exactly as badly as no key at all.",
        "keep it out of the prompt. It is a routing hint, not content, and it "
        "does not pin a request to a machine or guarantee a hit.",
        "then re-read these same hourly buckets. What should move first is the "
        "busy end: the gap between the quiet and busy shares closes before the "
        "average does.",
    ]


def window_start(days):
    """Floor to the hour so start_time lands on a bucket boundary."""
    now = dt.datetime.now(dt.timezone.utc).replace(minute=0, second=0, microsecond=0)
    return int((now - dt.timedelta(days=days)).timestamp())


def get(session, path, params=None):
    r = session.get(API + path, params=params or {}, timeout=60)
    if r.status_code in (401, 403):
        raise SystemExit("%d from OpenAI: /v1/organization/ needs an admin key "
                         "(sk-admin-...), not a project key" % r.status_code)
    r.raise_for_status()
    return r.json()


def read_buckets(session, path, params):
    """Walk the paginated usage endpoint."""
    params = dict(params)
    while True:
        page = get(session, path, params)
        for bucket in page.get("data") or []:
            yield bucket
        if not page.get("has_more") or not page.get("next_page"):
            return
        params["page"] = page["next_page"]


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--days", type=int, default=7,
                    help="days of hourly buckets to read (max 30)")
    ap.add_argument("--rho-floor", type=float, default=-0.4,
                    help="rank correlation at or below which the share is "
                         "treated as load-correlated")
    ap.add_argument("--show-all", action="store_true",
                    help="also print series that are behaving")
    args = ap.parse_args()

    admin = os.environ.get("OPENAI_ADMIN_KEY")
    if not admin:
        log.error("set OPENAI_ADMIN_KEY to an organization admin key "
                  "(sk-admin-...); a project key cannot read /v1/organization/")
        return 2

    days = max(2, min(int(args.days), 30))
    session = requests.Session()
    session.headers.update({"Authorization": "Bearer " + admin})

    buckets = read_buckets(session, "/organization/usage/completions", {
        "start_time": window_start(days),
        "bucket_width": "1h",
        "limit": 168,
        "group_by[]": ["project_id", "model"],
    })

    series = rows_by_series(buckets)
    if not series:
        log.info("no completions usage in the last %d day(s)", days)
        return 0

    checked = 0
    bad = 0
    for ident in sorted(series):
        rows = series[ident]
        state, detail = classify(rows, args.rho_floor)
        checked += 1
        line = "%-24s %s / %s  %s" % (state, ident[0], ident[1], detail)

        if state in FINDINGS:
            bad += 1
            log.warning(line)
            log.warning("  %d hour(s) that follow a gap in traffic were excluded "
                        "before correlating; those run cold for a different "
                        "reason.", len(resumption_rows(rows)))
            for repair in repair_lines():
                log.warning("  repair: %s", repair)
        else:
            note = handoff(state)
            if note:
                log.info(line)
                log.info("  %s", note)
            elif args.show_all:
                log.info(line)

    log.info("%d project/model series checked, %d finding(s)", checked, bad)
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())
openai-cache-key-routing-scatter.mjs
/**
 * Find OpenAI traffic whose cached share falls as concurrency rises.
 *
 * Read only. One paginated GET against the Usage API, which needs an admin key
 * (sk-admin-...). A project key is rejected by /v1/organization/.
 *
 * Cache lookup is prefix-based and routing-sensitive. Without prompt_cache_key
 * a fleet sprays byte-identical prompts across many backends and each one sees
 * a cold prefix, so the hit rate gets worse as you scale out. The signature is
 * a cached share negatively correlated with the hour's request count. A prefix
 * that is simply unstable gives a flat low share at every load, which is a
 * different note.
 *
 * Hours that follow a gap in traffic are dropped before anything is
 * correlated: they run cold because the entry was evicted while nobody was
 * calling, and leaving them in manufactures the very correlation being tested.
 */
const API = 'https://api.openai.com/v1';

const FINDINGS = new Set(['load-correlated-misses']);

/** Read a usage field as an integer. Pure. Missing and unreadable both mean 0. */
export function readInt(value) {
  const n = Number(value ?? 0);
  return Number.isFinite(n) ? Math.trunc(n) : 0;
}

/**
 * Hours since the epoch. Pure. Null if unreadable.
 * The buckets carry start_time as a unix integer, but the same code has to
 * survive an ISO string, and gap detection has to be integer arithmetic:
 * comparing formatted stamps gets 23:00 and 00:00 wrong every night.
 */
export function hourIndex(stamp) {
  if (typeof stamp === 'boolean' || stamp === null || stamp === undefined) return null;
  if (typeof stamp === 'number' && Number.isFinite(stamp)) {
    return Math.floor(Math.trunc(stamp) / 3600);
  }
  const text = String(stamp).trim().replace(' ', 'T');
  if (text.length < 13) return null;
  const head = text.slice(0, 13);
  if (!/^\d{4}-\d{2}-\d{2}T\d{2}$/.test(head)) return null;
  const when = Date.parse(`${head}:00:00Z`);
  if (Number.isNaN(when)) return null;
  return Math.floor(when / 3600000);
}

/** Render an hour index back as a UTC stamp. Pure. */
export function hourLabel(index) {
  if (index === null || index === undefined) return 'unknown';
  return `${new Date(Math.trunc(index) * 3600000).toISOString().slice(0, 13)}:00Z`;
}

/** Per project_id and model, one row per active hour, sorted. Pure. */
export function rowsBySeries(buckets) {
  const merged = new Map();
  for (const bucket of buckets ?? []) {
    const index = hourIndex(bucket?.start_time);
    if (index === null) continue;
    for (const result of bucket?.results ?? []) {
      if (!result || typeof result !== 'object') continue;
      const ident = `${result.project_id ?? 'unknown'}\t${result.model ?? 'unknown'}`;
      const cell = `${ident}\t${index}`;
      if (!merged.has(cell)) {
        merged.set(cell, { ident, index, hour: hourLabel(index),
                           requests: 0, input: 0, cached: 0 });
      }
      const row = merged.get(cell);
      row.requests += readInt(result.num_model_requests);
      row.input += readInt(result.input_tokens);
      row.cached += readInt(result.input_cached_tokens);
    }
  }
  const out = new Map();
  for (const row of merged.values()) {
    if (row.requests <= 0 && row.input <= 0) continue;
    if (!out.has(row.ident)) out.set(row.ident, []);
    out.get(row.ident).push(row);
  }
  for (const rows of out.values()) rows.sort((a, b) => a.index - b.index);
  return out;
}

/**
 * Pooled cached share over a set of hours. Pure. Null when nothing ran.
 * Pooled rather than averaged: an hour with nine requests must not carry the
 * same weight as an hour with nine thousand.
 */
export function cachedShare(rows) {
  let input = 0;
  let cached = 0;
  for (const row of rows ?? []) {
    input += readInt(row?.input);
    cached += readInt(row?.cached);
  }
  if (input <= 0) return null;
  return cached / input;
}

/**
 * Hours whose previous hour also carried traffic. Pure.
 * The exclusion that keeps this note off someone else's ground.
 */
export function continuationRows(rows) {
  const active = new Set((rows ?? []).map((r) => readInt(r?.index)));
  return (rows ?? []).filter((r) => active.has(readInt(r?.index) - 1));
}

/** The hours the correlation deliberately threw away. Pure. */
export function resumptionRows(rows) {
  const active = new Set((rows ?? []).map((r) => readInt(r?.index)));
  return (rows ?? []).filter((r) => !active.has(readInt(r?.index) - 1));
}

/** Average ranks, ties shared. Pure. */
function ranks(values) {
  const order = values.map((_v, i) => i).sort((a, b) => values[a] - values[b]);
  const out = new Array(values.length).fill(0);
  let i = 0;
  while (i < order.length) {
    let j = i;
    while (j + 1 < order.length && values[order[j + 1]] === values[order[i]]) j += 1;
    const shared = (i + j) / 2 + 1;
    for (let k = i; k <= j; k += 1) out[order[k]] = shared;
    i = j + 1;
  }
  return out;
}

/**
 * Rank correlation between two equal-length series. Pure. Null if flat.
 *
 * Rank rather than Pearson because request counts are heavy tailed and one
 * incident hour would otherwise decide the answer. The two degenerate cases
 * are deliberately different answers: a load that never varies returns null,
 * because nothing can be said about concurrency from a flat request rate,
 * while a share that never varies returns 0, because "no relationship" is a
 * real finding and it is the one that points at prefix instability.
 */
export function spearman(xs, ys) {
  const a = [...(xs ?? [])];
  const b = [...(ys ?? [])];
  if (a.length !== b.length || a.length < 8) return null;
  const rx = ranks(a);
  const ry = ranks(b);
  const n = a.length;
  const mx = rx.reduce((s, v) => s + v, 0) / n;
  const my = ry.reduce((s, v) => s + v, 0) / n;
  let sxy = 0;
  let sxx = 0;
  let syy = 0;
  for (let i = 0; i < n; i += 1) {
    sxy += (rx[i] - mx) * (ry[i] - my);
    sxx += (rx[i] - mx) ** 2;
    syy += (ry[i] - my) ** 2;
  }
  if (sxx <= 0) return null;
  if (syy <= 0) return 0;
  return sxy / (Math.sqrt(sxx) * Math.sqrt(syy));
}

/**
 * Pooled cached share in the quietest and busiest hours. Pure.
 * Returns [quietShare, busyShare, quietRate, busyRate], or nulls.
 */
export function loadSplit(rows, fraction = 0.33) {
  const active = (rows ?? []).filter((r) => readInt(r?.requests) > 0);
  if (active.length < 6) return [null, null, null, null];
  const ordered = [...active].sort((a, b) => readInt(a?.requests) - readInt(b?.requests));
  const size = Math.max(2, Math.trunc(ordered.length * fraction));
  const quiet = ordered.slice(0, size);
  const busy = ordered.slice(-size);
  const rate = (part) => part.reduce((s, r) => s + readInt(r?.requests), 0) / part.length;
  return [cachedShare(quiet), cachedShare(busy), rate(quiet), rate(busy)];
}

/** Which note owns this shape, when it is not this one. Pure. */
export function handoff(state) {
  if (state === 'no-cached-tokens') {
    return 'not one cached token at any load, so the traffic never becomes '
      + 'eligible rather than being routed away from its cache. Read the '
      + "prompt-below-model-cache-minimum note and check the mean input per "
      + "request against the model's floor first.";
  }
  if (state === 'flat-low-share') {
    return 'the share is low and stays low whatever the load, which is a prefix '
      + 'that differs between calls rather than requests landing on different '
      + 'machines. Read the cache-invalidated-by-changing-prefix note.';
  }
  if (state === 'cold-only-after-idle') {
    return 'the cold hours are the ones that follow gaps in traffic, and the '
      + 'busy hours are fine. That is eviction during idle time: read the '
      + 'prompt-cache-retention-left-at-default note.';
  }
  return '';
}

/** Classify one project and model series. Pure. Returns [state, detail]. */
export function classify(rows, rhoFloor = -0.4, ratioFloor = 0.6,
                         quietFloor = 0.15, minHours = 24) {
  const all = rows ?? [];
  const linked = continuationRows(all);
  if (linked.length < minHours) {
    return ['too-few-linked-hours',
      `${linked.length} hour(s) with traffic in the hour before them, under the `
      + `floor of ${minHours}. Correlating against load needs a run of busy hours.`];
  }

  const overall = cachedShare(linked);
  if (overall !== null && overall <= 0) {
    const input = linked.reduce((s, r) => s + readInt(r?.input), 0);
    return ['no-cached-tokens',
      `${input} input token(s) across ${linked.length} linked hour(s) and not one cached`];
  }

  const [quiet, busy, quietRate, busyRate] = loadSplit(linked);
  const rho = spearman(linked.map((r) => readInt(r?.requests)),
                       linked.map((r) => cachedShare([r]) ?? 0));

  if (rho === null || quiet === null || busy === null) {
    return ['load-does-not-vary',
      'the request rate barely moves across the window, so nothing here can be '
      + 'attributed to concurrency'];
  }

  if (rho <= rhoFloor && quiet >= quietFloor && busy <= quiet * ratioFloor) {
    return ['load-correlated-misses',
      `cached share ${(quiet * 100).toFixed(0)}% in the quietest hours `
      + `(${quietRate.toFixed(0)} req/h) and ${(busy * 100).toFixed(0)}% in the `
      + `busiest (${busyRate.toFixed(0)} req/h), rank correlation `
      + `${rho.toFixed(2)} against request rate. The prefix is cacheable; the `
      + 'requests are not landing where it is cached.'];
  }

  if (rho >= -rhoFloor) {
    return ['share-rises-with-load',
      `cached share climbs with the request rate (${rho.toFixed(2)}): density is `
      + 'keeping entries warm, which is the opposite of scatter'];
  }

  const cold = resumptionRows(all);
  const coldShare = cachedShare(cold);
  if (overall !== null && coldShare !== null && coldShare <= 0.02
      && overall >= quietFloor && cold.length >= 3) {
    return ['cold-only-after-idle',
      `${(overall * 100).toFixed(0)}% cached in linked hours against `
      + `${(coldShare * 100).toFixed(0)}% in the ${cold.length} hour(s) that follow a gap`];
  }

  if (overall !== null && overall < quietFloor) {
    return ['flat-low-share',
      `cached share ${(overall * 100).toFixed(0)}% overall with rank correlation `
      + `${rho.toFixed(2)} against load: low everywhere rather than low under load`];
  }

  return ['healthy',
    `cached share ${(quiet * 100).toFixed(0)}% quiet and ${(busy * 100).toFixed(0)}% `
    + `busy, correlation ${rho.toFixed(2)}`];
}

/** The routing hint, and what makes a good one. Pure. */
export function repairLines() {
  return [
    'set prompt_cache_key on the route: '
    + 'client.responses.create(..., prompt_cache_key="rag-answer-v3").',
    'make it coarse. The template name, or the template plus tenant, so traffic '
    + 'concentrates on a few caches. A per-request id scatters the fleet exactly '
    + 'as badly as no key at all.',
    'keep it out of the prompt. It is a routing hint, not content, and it does '
    + 'not pin a request to a machine or guarantee a hit.',
    'then re-read these same hourly buckets. What should move first is the busy '
    + 'end: the gap between the quiet and busy shares closes before the average does.',
  ];
}

function windowStart(days) {
  const now = new Date();
  now.setUTCMinutes(0, 0, 0);
  return Math.floor((now.getTime() - days * 86400000) / 1000);
}

async function get(key, path, params) {
  const url = new URL(API + path);
  for (const [k, v] of Object.entries(params ?? {})) {
    if (Array.isArray(v)) v.forEach((item) => url.searchParams.append(k, item));
    else url.searchParams.set(k, String(v));
  }
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
  if (res.status === 401 || res.status === 403) {
    throw new Error(`${res.status} from OpenAI: /v1/organization/ needs an admin `
                    + 'key (sk-admin-...), not a project key');
  }
  if (!res.ok) throw new Error(`${res.status} from ${path}`);
  return res.json();
}

async function* readBuckets(key, path, params) {
  let query = { ...params };
  for (;;) {
    const page = await get(key, path, query);
    for (const bucket of page?.data ?? []) yield bucket;
    if (!page?.has_more || !page?.next_page) return;
    query = { ...params, page: page.next_page };
  }
}

async function main() {
  const admin = process.env.OPENAI_ADMIN_KEY;
  if (!admin) {
    console.error('set OPENAI_ADMIN_KEY to an organization admin key '
                  + '(sk-admin-...); a project key cannot read /v1/organization/');
    process.exitCode = 2;
    return;
  }
  const days = Math.max(2, Math.min(Number(process.env.DAYS ?? 7), 30));
  const rhoFloor = Number(process.env.RHO_FLOOR ?? -0.4);
  const showAll = process.env.SHOW_ALL === '1';

  const buckets = [];
  for await (const bucket of readBuckets(admin, '/organization/usage/completions', {
    start_time: windowStart(days),
    bucket_width: '1h',
    limit: 168,
    'group_by[]': ['project_id', 'model'],
  })) buckets.push(bucket);

  const series = rowsBySeries(buckets);
  if (series.size === 0) {
    console.log(`no completions usage in the last ${days} day(s)`);
    return;
  }

  let checked = 0;
  let bad = 0;
  for (const ident of [...series.keys()].sort()) {
    const rows = series.get(ident);
    const [state, detail] = classify(rows, rhoFloor);
    checked += 1;
    const line = `${state.padEnd(24)} ${ident.replace('\t', ' / ')}  ${detail}`;

    if (FINDINGS.has(state)) {
      bad += 1;
      console.warn(line);
      console.warn(`  ${resumptionRows(rows).length} hour(s) that follow a gap in `
                   + 'traffic were excluded before correlating; those run cold '
                   + 'for a different reason.');
      for (const repair of repairLines()) console.warn(`  repair: ${repair}`);
    } else {
      const note = handoff(state);
      if (note) {
        console.log(line);
        console.log(`  ${note}`);
      } else if (showAll) {
        console.log(line);
      }
    }
  }

  console.log(`${checked} project/model series checked, ${bad} finding(s)`);
  process.exitCode = bad ? 1 : 0;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}

Add a test

Three fixtures share one traffic profile — a week of a normal daily load curve, quiet overnight and a broad afternoon peak — and differ only in how the cached share responds to it. One falls with load, one ignores it, one rises with it, and the three land on three different verdicts from byte-identical request counts. The fourth fixture is the trap: bursts that open at full tilt after a five-hour gap, where the busiest hour of every burst is also the coldest. Correlate that raw and it reads as a textbook scatter signature at -0.87. Drop the post-gap hours first and the linked hours are uniformly warm, and the script hands it to the retention note by name.

test_openai_cache_key_routing_scatter.py
from openai_cache_key_routing_scatter import (cached_share, classify,
                                              continuation_rows, handoff,
                                              hour_index, hour_label,
                                              load_split, resumption_rows,
                                              rows_by_series, spearman)

BASE = hour_index("2026-08-17T00:00Z")

# A daily traffic shape: quiet overnight, a broad afternoon peak.
LOAD = [200, 150, 120, 100, 120, 200, 400, 900, 1800, 3000, 3600, 3800,
        3900, 3800, 3600, 3000, 2400, 1800, 1200, 800, 600, 450, 350, 260]


def hour(offset, requests, share):
    tokens = requests * 2000
    return {"index": BASE + offset, "hour": hour_label(BASE + offset),
            "requests": requests, "input": tokens,
            "cached": int(round(tokens * share))}


def contiguous(share_of_load):
    """Seven contiguous days on the daily load shape, no gaps anywhere."""
    return [hour(i, LOAD[i % 24], share_of_load(LOAD[i % 24])) for i in range(168)]


# The note. The prefix is provably cacheable, because the quiet hours cache
# beautifully; the share falls away as the fleet fans out.
SCATTER = contiguous(lambda load: 0.72 - 0.00016 * load)
# Identical traffic, a share that ignores the load entirely. Prefix instability.
FLAT = contiguous(lambda _load: 0.10)
# Identical traffic, a share that improves with density. Not a fault.
RISING = contiguous(lambda load: 0.10 + 0.00016 * load)


def bursty():
    """Three-hour bursts five hours apart, each opening at full tilt.

    The busiest hour of every burst is also the coldest one, because it is the
    hour that follows the idle stretch. That is the trap: correlate the raw
    series and the load looks guilty.
    """
    shape = [(3000, 0.0), (1000, 0.7), (400, 0.7)]
    rows = []
    for burst in range(21):
        for step, (requests, share) in enumerate(shape):
            rows.append(hour(burst * 8 + step, requests, share))
    return rows


BURSTY = bursty()


def test_the_cached_share_falling_with_load_is_the_finding():
    # The note in one assertion: same prefix, same code, and the discount
    # evaporates exactly when the fleet is widest.
    quiet, busy, quiet_rate, busy_rate = load_split(SCATTER)
    assert round(quiet, 2) == 0.69 and round(busy, 2) == 0.16
    assert quiet_rate < 300 and busy_rate > 3400

    rho = spearman([r["requests"] for r in SCATTER],
                   [cached_share([r]) for r in SCATTER])
    assert round(rho, 3) == -1.0

    state, detail = classify(SCATTER)
    assert state == "load-correlated-misses"
    assert "68% in the quietest hours" in detail
    assert "16% in the busiest" in detail
    assert "rank correlation -1.00" in detail
    assert handoff(state) == ""


def test_the_same_traffic_with_a_flat_share_is_the_prefix_note():
    # The control that makes the finding mean something. Byte-identical load,
    # a share that does not move with it, and a different note owns it.
    assert [r["requests"] for r in FLAT] == [r["requests"] for r in SCATTER]
    assert spearman([r["requests"] for r in FLAT],
                    [cached_share([r]) for r in FLAT]) == 0.0
    state, detail = classify(FLAT)
    assert state == "flat-low-share"
    assert "low everywhere rather than low under load" in detail
    assert "cache-invalidated-by-changing-prefix" in handoff(state)


def test_a_share_that_climbs_with_load_is_not_scatter():
    state, detail = classify(RISING)
    assert state == "share-rises-with-load"
    assert "the opposite of scatter" in detail
    assert handoff(state) == ""


def test_hours_after_a_gap_are_excluded_before_correlating():
    # The exclusion, tested by the case it exists for. Leave the post-gap hours
    # in and the correlation goes sharply negative, because the cold hours are
    # also the ones that open a burst as it scales up. Take them out and the
    # linked hours are uniformly warm.
    everything = spearman([r["requests"] for r in BURSTY],
                          [cached_share([r]) for r in BURSTY])
    assert everything is not None and round(everything, 2) == -0.87

    assert len(continuation_rows(BURSTY)) == 42
    assert len(resumption_rows(BURSTY)) == 21
    assert cached_share(continuation_rows(BURSTY)) == 0.7
    assert cached_share(resumption_rows(BURSTY)) == 0.0

    state, detail = classify(BURSTY)
    assert state == "cold-only-after-idle"
    assert "70% cached in linked hours against 0% in the 21 hour(s)" in detail
    assert "prompt-cache-retention-left-at-default" in handoff(state)


def test_no_cached_tokens_at_any_load_is_an_eligibility_question():
    state, detail = classify(contiguous(lambda _load: 0.0))
    assert state == "no-cached-tokens"
    assert "not one cached" in detail
    assert "prompt-below-model-cache-minimum" in handoff(state)


def test_a_flat_request_rate_supports_no_verdict():
    # Nothing can be said about concurrency when the concurrency never moves,
    # and returning 0.0 there would read as "no relationship" rather than
    # "no evidence".
    steady = [hour(i, 1000, 0.6 if i % 2 else 0.2) for i in range(168)]
    assert spearman([r["requests"] for r in steady],
                    [cached_share([r]) for r in steady]) is None
    assert classify(steady)[0] == "load-does-not-vary"


def test_pooled_share_is_weighted_by_traffic():
    # An hour with nine requests must not outvote an hour with nine thousand.
    mixed = [hour(0, 10, 1.0), hour(1, 10_000, 0.1)]
    assert round(cached_share(mixed), 4) == 0.1009
    assert cached_share([]) is None
    assert cached_share([{"input": 0, "cached": 0}]) is None


def test_the_hour_index_survives_both_shapes_and_midnight():
    assert hour_index(1_755_388_800) == 1_755_388_800 // 3600
    assert hour_index("2026-08-17T23:00Z") + 1 == hour_index("2026-08-18T00:00Z")
    assert hour_label(hour_index("2026-08-17T09:00Z")) == "2026-08-17T09:00Z"
    assert hour_index("nonsense") is None
    assert hour_index(None) is None


def test_buckets_are_folded_into_project_and_model_series():
    buckets = [{"start_time": (BASE + i) * 3600,
                "results": [{"project_id": "proj_abc123", "model": "gpt-5.6",
                             "num_model_requests": LOAD[i % 24],
                             "input_tokens": LOAD[i % 24] * 2000,
                             "input_cached_tokens":
                                 int(LOAD[i % 24] * 2000
                                     * (0.72 - 0.00016 * LOAD[i % 24]))}]}
               for i in range(168)]
    series = rows_by_series(buckets)
    rows = series[("proj_abc123", "gpt-5.6")]
    assert len(rows) == 168
    assert [r["index"] for r in rows] == sorted(r["index"] for r in rows)
    assert classify(rows)[0] == "load-correlated-misses"


def test_thin_and_unreadable_windows_produce_no_verdict():
    assert classify([hour(i, 500, 0.5) for i in range(10)])[0] == "too-few-linked-hours"
    assert classify([])[0] == "too-few-linked-hours"
    assert classify(None)[0] == "too-few-linked-hours"
    assert spearman([1, 2], [1, 2]) is None
    assert spearman([1, 2, 3], None) is None
    assert load_split([]) == (None, None, None, None)
    assert rows_by_series([{"start_time": "bad", "results": []}]) == {}
openai-cache-key-routing-scatter.test.mjs
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { cachedShare, classify, continuationRows, handoff, hourIndex, hourLabel,
         loadSplit, resumptionRows, rowsBySeries, spearman }
  from './openai-cache-key-routing-scatter.mjs';

const BASE = hourIndex('2026-08-17T00:00Z');

const LOAD = [200, 150, 120, 100, 120, 200, 400, 900, 1800, 3000, 3600, 3800,
              3900, 3800, 3600, 3000, 2400, 1800, 1200, 800, 600, 450, 350, 260];

const hour = (offset, requests, share) => {
  const tokens = requests * 2000;
  return { index: BASE + offset, hour: hourLabel(BASE + offset), requests,
           input: tokens, cached: Math.round(tokens * share) };
};

const contiguous = (shareOfLoad) => Array.from({ length: 168 },
  (_, i) => hour(i, LOAD[i % 24], shareOfLoad(LOAD[i % 24])));

const SCATTER = contiguous((load) => 0.72 - 0.00016 * load);
const FLAT = contiguous(() => 0.10);
const RISING = contiguous((load) => 0.10 + 0.00016 * load);

// Three-hour bursts five hours apart, each opening at full tilt: the busiest
// hour of every burst is also the coldest, because it follows the idle stretch.
const BURSTY = (() => {
  const shape = [[3000, 0.0], [1000, 0.7], [400, 0.7]];
  const rows = [];
  for (let burst = 0; burst < 21; burst += 1) {
    shape.forEach(([requests, share], step) => {
      rows.push(hour(burst * 8 + step, requests, share));
    });
  }
  return rows;
})();

test('the cached share falling with load is the finding', () => {
  const [quiet, busy, quietRate, busyRate] = loadSplit(SCATTER);
  assert.equal(Number(quiet.toFixed(2)), 0.69);
  assert.equal(Number(busy.toFixed(2)), 0.16);
  assert.ok(quietRate < 300 && busyRate > 3400);

  const rho = spearman(SCATTER.map((r) => r.requests),
                       SCATTER.map((r) => cachedShare([r])));
  assert.equal(Number(rho.toFixed(3)), -1);

  const [state, detail] = classify(SCATTER);
  assert.equal(state, 'load-correlated-misses');
  assert.match(detail, /68% in the quietest hours/);
  assert.match(detail, /16% in the busiest/);
  assert.match(detail, /rank correlation -1\.00/);
  assert.equal(handoff(state), '');
});

test('the same traffic with a flat share is the prefix note', () => {
  assert.deepEqual(FLAT.map((r) => r.requests), SCATTER.map((r) => r.requests));
  assert.equal(spearman(FLAT.map((r) => r.requests),
                        FLAT.map((r) => cachedShare([r]))), 0);
  const [state, detail] = classify(FLAT);
  assert.equal(state, 'flat-low-share');
  assert.match(detail, /low everywhere rather than low under load/);
  assert.match(handoff(state), /cache-invalidated-by-changing-prefix/);
});

test('a share that climbs with load is not scatter', () => {
  const [state, detail] = classify(RISING);
  assert.equal(state, 'share-rises-with-load');
  assert.match(detail, /the opposite of scatter/);
  assert.equal(handoff(state), '');
});

test('hours after a gap are excluded before correlating', () => {
  const everything = spearman(BURSTY.map((r) => r.requests),
                              BURSTY.map((r) => cachedShare([r])));
  assert.equal(Number(everything.toFixed(2)), -0.87);

  assert.equal(continuationRows(BURSTY).length, 42);
  assert.equal(resumptionRows(BURSTY).length, 21);
  assert.equal(cachedShare(continuationRows(BURSTY)), 0.7);
  assert.equal(cachedShare(resumptionRows(BURSTY)), 0);

  const [state, detail] = classify(BURSTY);
  assert.equal(state, 'cold-only-after-idle');
  assert.match(detail, /70% cached in linked hours against 0% in the 21 hour\(s\)/);
  assert.match(handoff(state), /prompt-cache-retention-left-at-default/);
});

test('no cached tokens at any load is an eligibility question', () => {
  const [state, detail] = classify(contiguous(() => 0));
  assert.equal(state, 'no-cached-tokens');
  assert.match(detail, /not one cached/);
  assert.match(handoff(state), /prompt-below-model-cache-minimum/);
});

test('a flat request rate supports no verdict', () => {
  const steady = Array.from({ length: 168 },
    (_, i) => hour(i, 1000, i % 2 ? 0.6 : 0.2));
  assert.equal(spearman(steady.map((r) => r.requests),
                        steady.map((r) => cachedShare([r]))), null);
  assert.equal(classify(steady)[0], 'load-does-not-vary');
});

test('pooled share is weighted by traffic', () => {
  const mixed = [hour(0, 10, 1.0), hour(1, 10000, 0.1)];
  assert.equal(Number(cachedShare(mixed).toFixed(4)), 0.1009);
  assert.equal(cachedShare([]), null);
  assert.equal(cachedShare([{ input: 0, cached: 0 }]), null);
});

test('the hour index survives both shapes and midnight', () => {
  assert.equal(hourIndex(1755388800), Math.floor(1755388800 / 3600));
  assert.equal(hourIndex('2026-08-17T23:00Z') + 1, hourIndex('2026-08-18T00:00Z'));
  assert.equal(hourLabel(hourIndex('2026-08-17T09:00Z')), '2026-08-17T09:00Z');
  assert.equal(hourIndex('nonsense'), null);
  assert.equal(hourIndex(null), null);
});

test('buckets are folded into project and model series', () => {
  const buckets = Array.from({ length: 168 }, (_, i) => ({
    start_time: (BASE + i) * 3600,
    results: [{ project_id: 'proj_abc123', model: 'gpt-5.6',
                num_model_requests: LOAD[i % 24],
                input_tokens: LOAD[i % 24] * 2000,
                input_cached_tokens: Math.trunc(LOAD[i % 24] * 2000
                  * (0.72 - 0.00016 * LOAD[i % 24])) }],
  }));
  const rows = rowsBySeries(buckets).get('proj_abc123\tgpt-5.6');
  assert.equal(rows.length, 168);
  assert.deepEqual(rows.map((r) => r.index), [...rows.map((r) => r.index)].sort((a, b) => a - b));
  assert.equal(classify(rows)[0], 'load-correlated-misses');
});

test('thin and unreadable windows produce no verdict', () => {
  const thin = Array.from({ length: 10 }, (_, i) => hour(i, 500, 0.5));
  assert.equal(classify(thin)[0], 'too-few-linked-hours');
  assert.equal(classify([])[0], 'too-few-linked-hours');
  assert.equal(classify(null)[0], 'too-few-linked-hours');
  assert.equal(spearman([1, 2], [1, 2]), null);
  assert.equal(spearman([1, 2, 3], null), null);
  assert.deepEqual(loadSplit([]), [null, null, null, null]);
  assert.equal(rowsBySeries([{ start_time: 'bad', results: [] }]).size, 0);
});

FAQ

Why is a low cached share not enough on its own?

Because four different faults produce one. A prompt under the model's floor never caches at all. A prefix that changes between calls caches and never matches. An entry evicted during idle time is gone before the next run. And requests scattered across a fleet miss a cache that exists and is warm. Every one of those shows up as the same low number in the same field, and each has a different repair. The slope against load is what separates the fourth from the other three: it is the only one of the four that gets worse as you add capacity.

What does prompt_cache_key actually do?

It influences routing so that requests carrying the same key are more likely to reach the same cache. The documentation is careful about this and so should you be: it does not pin a request to a machine and it does not guarantee a cache read. It is a hint that raises the odds, and the effect is statistical and visible in aggregate over hours rather than deterministic per call. It is also not part of the prompt, so it does not change the prefix and cannot invalidate anything by being set.

How coarse should the key be?

Coarse enough to concentrate traffic and no coarser. The template name, or the template name plus a tenant id, is usually right. A per-request id is the classic mistake: it looks like a cache key, it satisfies whatever review asked for one, and it scatters the fleet exactly as badly as having no key at all, because no two requests ever share it. If you find yourself putting anything that varies per call into it, you have built a request id.

Why exclude the hours that follow a gap?

Because they run cold for a reason that has nothing to do with routing, and because scheduled work tends to open at its busiest. A nightly batch that fires hard at 02:00 after six idle hours produces a cold hour and a high request count at the same moment, which is a textbook negative correlation with no routing problem anywhere in it. The tests build exactly that series and check the script does not claim it. The count of dropped hours is printed so the exclusion is visible rather than assumed.

Should the cached share climb as traffic grows?

On a well-keyed fleet, yes, and the script reports that case rather than staying quiet about it. Density is what keeps entries warm, so busy hours ought to cache better than quiet ones. That is the shape to aim for after the repair, and it is a useful thing to be able to point at: a positive correlation is evidence that the prefix is stable and the routing hint is landing, which is a stronger statement than an average that happens to look acceptable this week.

Related field notes

Sources

Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.

Stuck on a tricky one?

If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.