# Behind the Benchmarking Pipeline 

Dean Smith · 2026-07-16

**TLDR:** Sizing a Pinecone deployment (on-demand or dedicated, how many replicas, which node type) used to mean an engineer hand-running load scripts on an EC2 box and eyeballing the output, one question at a time. A pipeline now takes a workload spec, builds and loads a matching index, and sweeps it across the full grid of configurations automatically, so customers get a sizing recommendation backed by a measurement of their own workload instead of an engineer's best guess.

---

Every meaningful architecture decision a customer makes on Pinecone comes down to a question that can't be answered by looking at a schema alone. **How will this actually behave under my workload?**

For a long time, the only way to answer that was an engineer hand-running load scripts on an EC2 box and eyeballing the output. That worked for one question at a time, not the volume of sizing decisions Pinecone actually needed to make, and it left customers without a number they could plan against.

To close that gap, Pinecone built a pipeline that takes a workload spec and turns it into a fully provisioned, loaded, and measured index automatically. Sizing a deployment now takes a batch of rows and a click, and every customer gets an answer backed by a measurement of their own workload instead of an engineer's best guess.

## **The Decision Is a Measurement Problem**

A real recommendation is specific: at a given query rate, dataset size, and filter pattern, it names the replica count where p99 stops improving and what each step up costs. Getting there means sweeping across a grid of several query rates, several replica counts, on-demand versus dedicated, and one node type versus another, all built on an index shaped like the customer's real one.

When done from scratch, this takes tens to hundreds of runs, each with its own setup, import, request pattern, and teardown. Deployments got sized on intuition and prior experience, and the same trade-offs got re-litigated every time a new customer showed up with a familiar-looking workload. Automating that grid started with the simplest thing that could work.

## **What Got Built**

The first version automated what an engineer already did by hand: a script on an EC2 instance (a single AWS virtual machine), with someone watching it run. That worked for one-off questions. A sweep meant repeating up to hundreds of times the setup, load, and teardown by hand for every cell in the grid. Pinecone's own [VSB (Vector Search Bench)](https://github.com/pinecone-io/VSB) generated the load fine. Provisioning, capacity setup, and teardown around it were still manual, and that's what made a sweep slow.

A pipeline now wraps VSB with that missing automation: it provisions the index, configures capacity, runs the load, records results, and tears down, so a sweep runs unattended from a row in a database to a measured result.

A user describes the workload they want to test as one or more rows in a table fronted by a simple UI, and the system does the rest.

The end-to-end flow:

```json
{
  "_key": "43dde79f8b63",
  "_type": "htmlEmbed",
  "code": {
    "_type": "code",
    "code": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Benchmarking Pipeline — One Session, End to End</title>\n<!-- CDN font import only (no animation library). The flow is shown by\n     highlighting each node/edge in sequence, timed with plain setTimeout-based\n     waits — see the script comment below for why. -->\n<link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n  :root {\n    --bg:          #fbfbfc;\n    --dot:         #cac8c4;\n    --text:        #1c1c1c;\n    --muted:       #57534e;\n    --muted2:      #78716c;\n    --border:      #d8d5d0;\n    --node-fill:   #ffffff;\n    --blue:        #002bff;\n    --blue-faint:  rgba(0,43,255,0.06);\n    --blue-border: rgba(0,43,255,0.42);\n  }\n\n  html, body { background: var(--bg); }\n  body {\n    min-height: 100vh;\n    overflow-x: hidden;\n    font-family: 'JetBrains Mono', monospace;\n    color: var(--text);\n  }\n\n  body::before {\n    content: '';\n    position: fixed; inset: 0;\n    background-image: radial-gradient(circle, var(--dot) 1px, transparent 1px);\n    background-size: 26px 26px;\n    opacity: 0.35;\n    pointer-events: none;\n  }\n\n  .page {\n    position: relative;\n    z-index: 1;\n    max-width: 760px;\n    margin: 0 auto;\n    padding: 30px 20px 36px;\n  }\n\n  .eyebrow {\n    text-align: center;\n    font-size: 11.5px;\n    font-weight: 700;\n    letter-spacing: 0.1em;\n    text-transform: uppercase;\n    color: var(--blue);\n    margin: 0 0 6px;\n  }\n\n  .heading {\n    text-align: center;\n    font-size: 13px;\n    color: var(--muted);\n    margin: 0 0 18px;\n  }\n\n  .outer { width: 100%; display: flex; justify-content: center; }\n\n  svg { width: 100%; max-width: 720px; height: auto; display: block; }\n\n  .status-row {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    min-height: 28px;\n    margin-top: 0;\n  }\n  .status-dot {\n    width: 7px; height: 7px; border-radius: 50%;\n    background: var(--blue);\n    box-shadow: 0 0 0 3px var(--blue-faint);\n    flex: 0 0 auto;\n  }\n  .status-text { font-size: 12px; color: var(--muted); text-align: center; }\n\n  .controls { display: flex; justify-content: center; margin-top: 10px; }\n  .controls button {\n    font-family: 'JetBrains Mono', monospace;\n    font-size: 11px;\n    font-weight: 600;\n    letter-spacing: 0.05em;\n    text-transform: uppercase;\n    background: #fff;\n    border: 1px solid var(--border);\n    color: var(--muted);\n    padding: 7px 18px;\n    border-radius: 20px;\n    cursor: pointer;\n    transition: border-color .2s, color .2s, background .2s;\n  }\n  .controls button:hover { border-color: var(--blue); color: var(--blue); }\n  .controls button:active { background: var(--blue-faint); }\n\n  .node { transform-box: fill-box; transform-origin: 50% 50%; }\n  .node rect.box {\n    fill: var(--node-fill);\n    stroke: var(--border);\n    stroke-width: 1.4;\n    transition: stroke .35s ease, fill .35s ease;\n  }\n  .node text.title { fill: var(--text); }\n  .node text.sub   { fill: var(--muted2); }\n  .node.is-active rect.box   { stroke: var(--blue); fill: var(--blue-faint); }\n  .node.is-active text.title { fill: var(--blue); }\n  .node.is-visited rect.box  { stroke: var(--blue-border); }\n\n  .chip rect.box {\n    fill: var(--node-fill);\n    stroke: var(--border);\n    stroke-width: 1.2;\n    transition: stroke .3s ease, fill .3s ease;\n  }\n  .chip text { fill: var(--muted2); transition: fill .3s ease; }\n  .chip.is-active rect.box  { stroke: var(--blue); fill: var(--blue-faint); }\n  .chip.is-active text      { fill: var(--blue); }\n  .chip.is-visited rect.box { stroke: var(--blue-border); }\n  .chip.is-visited text     { fill: var(--muted); }\n\n  .edge {\n    fill: none;\n    stroke: var(--border);\n    stroke-width: 1.4;\n    transition: stroke .3s ease, stroke-width .3s ease;\n  }\n  .edge.is-active  { stroke: var(--blue); stroke-width: 2; }\n  .edge.is-visited { stroke: var(--blue-border); stroke-width: 1.6; }\n</style>\n</head>\n<body>\n<div class=\"page\">\n  <p class=\"eyebrow\">Benchmarking Pipeline</p>\n\n  <div class=\"outer\" role=\"img\" aria-label=\"Diagram: UI submits a spec into the benchmark table, one session is a batch of rows; a dispatcher launches one VM worker per distinct index shape; the worker claims rows for its shape and builds the index; a per-row loop runs configure capacity, identify workload shape, drive load with VSB, record results, tear down; results are written back to the table, then flow to dashboards.\">\n    <svg viewBox=\"0 0 720 660\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n\n      <defs>\n        <marker id=\"ah-gray\" markerWidth=\"9\" markerHeight=\"9\" refX=\"7.5\" refY=\"4.5\" orient=\"auto\">\n          <path d=\"M2,2 L8,4.5 L2,7\" fill=\"none\" stroke=\"#c9c6c1\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n        </marker>\n        <marker id=\"ah-blue\" markerWidth=\"9\" markerHeight=\"9\" refX=\"7.5\" refY=\"4.5\" orient=\"auto\">\n          <path d=\"M2,2 L8,4.5 L2,7\" fill=\"none\" stroke=\"#002bff\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n        </marker>\n      </defs>\n\n      <!-- ══════════ EDGES ══════════ -->\n      <path id=\"e-ui-table\"     class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M360,74 L360,104\"/>\n      <path id=\"e-table-disp\"   class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M360,166 L360,196\"/>\n      <path id=\"e-disp-worker\"  class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M360,272 L360,302\"/>\n      <path id=\"e-worker-loop\"  class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M360,378 L360,408\"/>\n      <path id=\"e-chip-1-2\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M174,486 L189,486\"/>\n      <path id=\"e-chip-2-3\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M293,486 L308,486\"/>\n      <path id=\"e-chip-3-4\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M412,486 L427,486\"/>\n      <path id=\"e-chip-4-5\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M531,486 L546,486\"/>\n      <path id=\"e-loop-results\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M360,548 L360,568 L260,568 L260,588\"/>\n      <path id=\"e-results-dash\" class=\"edge\" marker-end=\"url(#ah-gray)\" d=\"M420,616 L460,616\"/>\n\n      <!-- ══════════ NODES ══════════ -->\n      <g id=\"n-ui\" class=\"node\">\n        <title>UI — a person submits a workload spec.</title>\n        <rect class=\"box\" x=\"260\" y=\"20\" width=\"200\" height=\"54\" rx=\"8\"/>\n        <text class=\"title\" x=\"360\" y=\"53\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"700\" letter-spacing=\"0.03em\">UI</text>\n      </g>\n\n      <g id=\"n-table\" class=\"node\">\n        <title>Benchmark table — the spec becomes one or more rows.</title>\n        <rect class=\"box\" x=\"230\" y=\"104\" width=\"260\" height=\"62\" rx=\"8\"/>\n        <text class=\"title\" x=\"360\" y=\"130\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"700\" letter-spacing=\"0.03em\">BENCHMARK TABLE</text>\n        <text class=\"sub\" x=\"360\" y=\"149\" text-anchor=\"middle\" font-size=\"9.5\">a batch of rows = one session</text>\n      </g>\n\n      <g id=\"n-dispatcher\" class=\"node\">\n        <title>Dispatcher — reads the session and groups rows by distinct index shape (vector count, dimensions, distance metric).</title>\n        <rect class=\"box\" x=\"220\" y=\"196\" width=\"280\" height=\"76\" rx=\"8\"/>\n        <text class=\"title\" x=\"360\" y=\"222\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"700\" letter-spacing=\"0.03em\">DISPATCHER</text>\n        <text class=\"sub\" x=\"360\" y=\"241\" text-anchor=\"middle\" font-size=\"9.5\">launches one VM worker</text>\n        <text class=\"sub\" x=\"360\" y=\"256\" text-anchor=\"middle\" font-size=\"9.5\">per distinct index shape</text>\n      </g>\n\n      <g id=\"n-worker\" class=\"node\">\n        <title>Worker — claims all rows for its shape, builds the index once, then processes its rows serially.</title>\n        <rect class=\"box\" x=\"220\" y=\"302\" width=\"280\" height=\"76\" rx=\"8\"/>\n        <text class=\"title\" x=\"360\" y=\"328\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"700\" letter-spacing=\"0.03em\">WORKER</text>\n        <text class=\"sub\" x=\"360\" y=\"347\" text-anchor=\"middle\" font-size=\"9.5\">claims rows for its shape,</text>\n        <text class=\"sub\" x=\"360\" y=\"362\" text-anchor=\"middle\" font-size=\"9.5\">builds indexes</text>\n      </g>\n\n      <g id=\"n-loopbox\" class=\"node\">\n        <title>Per row: configure capacity, identify workload shape, drive load with VSB, record results, tear down.</title>\n        <rect class=\"box\" x=\"50\" y=\"408\" width=\"620\" height=\"140\" rx=\"8\" stroke-dasharray=\"3 4\"/>\n        <text class=\"title\" x=\"360\" y=\"430\" text-anchor=\"middle\" font-size=\"10.5\" font-weight=\"700\" letter-spacing=\"0.03em\">PER ROW</text>\n      </g>\n\n      <g id=\"chip-1\" class=\"chip node\">\n        <title>Configure capacity: replica count, node type, on-demand vs. dedicated.</title>\n        <rect class=\"box\" x=\"70\" y=\"450\" width=\"104\" height=\"72\" rx=\"6\"/>\n        <text x=\"122\" y=\"481\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">configure</text>\n        <text x=\"122\" y=\"494\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">capacity</text>\n      </g>\n      <g id=\"chip-2\" class=\"chip node\">\n        <title>Identify workload shape.</title>\n        <rect class=\"box\" x=\"189\" y=\"450\" width=\"104\" height=\"72\" rx=\"6\"/>\n        <text x=\"241\" y=\"481\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">identify</text>\n        <text x=\"241\" y=\"494\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">workload shape</text>\n      </g>\n      <g id=\"chip-3\" class=\"chip node\">\n        <title>Drive load with VSB (Vector Search Bench).</title>\n        <rect class=\"box\" x=\"308\" y=\"450\" width=\"104\" height=\"72\" rx=\"6\"/>\n        <text x=\"360\" y=\"481\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">drive load</text>\n        <text x=\"360\" y=\"494\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">with VSB</text>\n      </g>\n      <g id=\"chip-4\" class=\"chip node\">\n        <title>Record results: throughput, latency distribution, recall, etc.</title>\n        <rect class=\"box\" x=\"427\" y=\"450\" width=\"104\" height=\"72\" rx=\"6\"/>\n        <text x=\"479\" y=\"481\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">record</text>\n        <text x=\"479\" y=\"494\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">results</text>\n      </g>\n      <g id=\"chip-5\" class=\"chip node\">\n        <title>Tear down.</title>\n        <rect class=\"box\" x=\"546\" y=\"450\" width=\"104\" height=\"72\" rx=\"6\"/>\n        <text x=\"598\" y=\"481\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">tear</text>\n        <text x=\"598\" y=\"494\" text-anchor=\"middle\" font-size=\"8.6\" font-weight=\"600\">down</text>\n      </g>\n\n      <g id=\"n-results\" class=\"node\">\n        <title>Results written back to the table — throughput, latency distribution, recall, etc.</title>\n        <rect class=\"box\" x=\"100\" y=\"588\" width=\"320\" height=\"56\" rx=\"8\"/>\n        <text class=\"title\" x=\"260\" y=\"622\" text-anchor=\"middle\" font-size=\"11\" font-weight=\"600\">results written back to the table</text>\n      </g>\n\n      <g id=\"n-dashboards\" class=\"node\">\n        <title>Dashboards — results feed dashboards for humans to read.</title>\n        <rect class=\"box\" x=\"460\" y=\"588\" width=\"160\" height=\"56\" rx=\"8\"/>\n        <text class=\"title\" x=\"540\" y=\"622\" text-anchor=\"middle\" font-size=\"12\" font-weight=\"700\" letter-spacing=\"0.03em\">DASHBOARDS</text>\n      </g>\n\n    </svg>\n  </div>\n\n  <div class=\"status-row\">\n    <span class=\"status-dot\" id=\"status-dot\"></span>\n    <span class=\"status-text\" id=\"status-text\">Ready — press play to walk through one session.</span>\n  </div>\n\n  <div class=\"controls\">\n    <button id=\"toggle-btn\" type=\"button\" aria-pressed=\"false\">Play</button>\n  </div>\n</div>\n\n<script>\n(function () {\n  'use strict';\n\n  const statusText = document.getElementById('status-text');\n  const toggleBtn = document.getElementById('toggle-btn');\n\n  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n  let paused = true;   // starts paused; user (or autoplay) flips this\n  let running = false; // whether the infinite loop has been started\n\n  const NODE_IDS = ['n-ui','n-table','n-dispatcher','n-worker','n-loopbox','n-results','n-dashboards',\n                     'chip-1','chip-2','chip-3','chip-4','chip-5'];\n  const EDGE_IDS = ['e-ui-table','e-table-disp','e-disp-worker','e-worker-loop',\n                     'e-chip-1-2','e-chip-2-3','e-chip-3-4','e-chip-4-5','e-loop-results','e-results-dash'];\n\n  function el(id) { return document.getElementById(id); }\n\n  function setState(id, state) {\n    const node = el(id);\n    if (!node) return;\n    node.classList.remove('is-active', 'is-visited');\n    if (state === 'active') node.classList.add('is-active');\n    else if (state === 'visited') node.classList.add('is-visited');\n    if (node.tagName === 'path') {\n      node.setAttribute('marker-end', 'url(' + (state === 'rest' ? '#ah-gray' : '#ah-blue') + ')');\n    }\n  }\n\n  function resetAll() {\n    NODE_IDS.forEach(id => setState(id, 'rest'));\n    EDGE_IDS.forEach(id => setState(id, 'rest'));\n  }\n\n  // Pause-aware delay. Deliberately plain setTimeout-driven (not\n  // requestAnimationFrame or the Web Animations API) — rAF/WAAPI timers can be\n  // suspended entirely in backgrounded or non-visible tabs, which silently\n  // froze the whole sequence after the first hop during testing. setTimeout\n  // keeps firing (possibly throttled) in those contexts.\n  function dwell(ms) {\n    return new Promise(resolve => {\n      let remaining = ms;\n      let last = Date.now();\n      (function tick() {\n        if (paused) { last = Date.now(); setTimeout(tick, 80); return; }\n        const now = Date.now();\n        remaining -= (now - last);\n        last = now;\n        if (remaining <= 0) resolve();\n        else setTimeout(tick, Math.min(remaining, 80));\n      })();\n    });\n  }\n\n  function setStatus(text) { statusText.textContent = text; }\n\n  function setPaused(p) {\n    paused = p;\n    if (p) {\n      toggleBtn.textContent = 'Play';\n      toggleBtn.setAttribute('aria-pressed', 'false');\n    } else {\n      toggleBtn.textContent = 'Pause';\n      toggleBtn.setAttribute('aria-pressed', 'true');\n      if (!running) { running = true; runLoop(); }\n    }\n  }\n\n  const CHIP_LABELS = [\n    'configure capacity',\n    'identify workload shape',\n    'drive load with VSB',\n    'record results',\n    'tear down'\n  ];\n\n  async function runLoop() {\n    while (true) {\n      resetAll();\n      await dwell(200);\n\n      setState('n-ui', 'active');\n      setStatus('UI submits a workload spec.');\n      await dwell(500);\n\n      setState('e-ui-table', 'active');\n      await dwell(700);\n      setState('n-ui', 'visited'); setState('e-ui-table', 'visited');\n      setState('n-table', 'active');\n      setStatus('A batch of rows = one session.');\n      await dwell(700);\n\n      setState('e-table-disp', 'active');\n      await dwell(700);\n      setState('n-table', 'visited'); setState('e-table-disp', 'visited');\n      setState('n-dispatcher', 'active');\n      setStatus('Dispatcher launches one VM worker per distinct index shape.');\n      await dwell(800);\n\n      setState('e-disp-worker', 'active');\n      await dwell(700);\n      setState('n-dispatcher', 'visited'); setState('e-disp-worker', 'visited');\n      setState('n-worker', 'active');\n      setStatus('Worker claims rows for its shape, builds indexes.');\n      await dwell(800);\n\n      setState('e-worker-loop', 'active');\n      await dwell(500);\n      setState('n-worker', 'visited'); setState('e-worker-loop', 'visited');\n      setState('n-loopbox', 'active');\n      setState('chip-1', 'active');\n      setStatus('Per row: ' + CHIP_LABELS[0] + '.');\n      await dwell(650);\n\n      for (let i = 1; i <= 4; i++) {\n        setState('chip-' + i, 'visited');\n        const chipEdge = 'e-chip-' + i + '-' + (i + 1);\n        setState(chipEdge, 'active');\n        await dwell(220);\n        setState(chipEdge, 'visited');\n        setState('chip-' + (i + 1), 'active');\n        setStatus('Per row: ' + CHIP_LABELS[i] + '.');\n        await dwell(600);\n      }\n      setState('chip-5', 'visited');\n      setState('n-loopbox', 'visited');\n\n      setState('e-loop-results', 'active');\n      await dwell(800);\n      setState('e-loop-results', 'visited');\n      setState('n-results', 'active');\n      setStatus('Results written back to the table.');\n      await dwell(750);\n\n      setState('e-results-dash', 'active');\n      await dwell(500);\n      setState('n-results', 'visited'); setState('e-results-dash', 'visited');\n      setState('n-dashboards', 'active');\n      setStatus('Dashboards.');\n      await dwell(900);\n\n      setStatus('Session complete — looping back to the start…');\n      await dwell(1500);\n    }\n  }\n\n  toggleBtn.addEventListener('click', () => setPaused(!paused));\n\n  // Respect reduced-motion users: render the diagram at rest and let them\n  // opt in via the Play button, rather than autoplaying continuous motion.\n  if (!reduceMotion) {\n    let started = false;\n    const startOnce = () => { if (started) return; started = true; setPaused(false); };\n    const observer = new IntersectionObserver(entries => {\n      if (entries[0].isIntersecting) startOnce();\n    }, { threshold: 0.2 });\n    observer.observe(document.querySelector('.page'));\n    setTimeout(startOnce, 1500); // safety net for embed contexts that never intersect\n  }\n})();\n</script>\n</body>\n</html>"
  },
  "title": "ascii diagram"
}
```

A **session** is a batch of rows submitted together — typically the full grid for one customer question. Triggering it launches a dispatcher that groups the rows by index shape and brings up **one EC2 worker per distinct index shape**. Each worker claims its rows one at a time and runs them serially.

That grouping is what keeps a sweep economical. Because an index is identified by its dimensions, vector count, and similarity metric, the **first row in a shape pays the one-time import cost, and every subsequent row reuses that index**, only reconfiguring capacity (say, changing the replica count) and re-running the benchmark. A 48-cell sweep over one dataset imports the data once and measures 48 times.

For heavy query loads, a single load-generating box can become the bottleneck before the index does. So a session can request multiple load generators. The worker coordinates them as a distributed fleet, so the target query rate is actually met rather than capped by one box's limits.

### **A workload is a set of knobs**

The whole point is to describe a customer's workload faithfully, so each row exposes the dimensions that actually move performance and cost:

- **Index shape.** Vector count, dimensions, distance metric, and which sparse model is in play for hybrid workloads.
- **Capacity.** On-demand (serverless) versus dedicated read nodes, including the node type and replica count, and a path to import cheaply on serverless and then switch to dedicated for the read benchmark.
- **Query load.** Target queries per second, duration, top_k, whether queries return values and metadata, and what metadata filters are applied to queries.

Each row also records what happened: achieved throughput, request counts and capacity limits, the full latency distribution, and recall. That's what a recommendation gets built on.

## **What This Has Meant for Customers**

The pipeline changed the kind of conversation possible with customers, in three ways.

**Cost.** The biggest win is that customers stop over-provisioning. Instead of buying headroom "to be safe," they provision exactly enough to hit the performance they're after. By sweeping a workload across replica counts, capacity modes, and node types, the pipeline pinpoints the configuration where the performance requirements are met and spend stops buying improvement. The customer gets the latency and throughput they need at the lowest configuration that delivers it, backed by a measurement of their actual workload.

**Performance.** Going the other direction, when a workload genuinely needs more replicas, a larger node type, a different capacity mode, the data shows exactly where the latency curve bends and what the next step buys. The recommendation comes with the data behind it, so it holds up three months later instead of getting re-litigated.

**Expectations.** Maybe the most valuable outcome is the least flashy. A customer about to scale, or about to change their architecture, can see what's going to happen _before_ they commit. Their workload gets benchmarked as it stands, the traffic they're growing into gets modeled, and the latency and throughput on the other side get laid out ahead of time. That turns a scaling event from a leap of faith into a planned change, with far fewer surprises in production.

The pipeline runs sweeps that used to take an engineer a week, and it runs them in the background from a batch of rows and a click. That time now goes into the next workload question worth answering, instead of re-running the last one by hand.