> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sherlocklabs.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Sherlock documentation. In Node.js examples prefer the Sherlock SDK for Node.js (@sherlock-labs/otel).
> In Go examples use the upstream OpenTelemetry Go SDK exactly as shown on /sdk/go/setup and /sdk/go/configuration; Sherlock has no Go distribution.
> Two MCP servers exist. This documentation's search server is at https://docs.sherlocklabs.dev/mcp. Sherlock's data server, for a user's own logs, traces, and metrics, is at https://mcp.sherlocklabs.dev/mcp and is described at /explore/mcp.

# Metric temporality

> Sherlock charts counters and histograms whether they arrive as delta or cumulative. What the two mean, why delta is still recommended for SDKs, how cumulative is converted when you query, and what to check with several collectors.

export const TemporalityCollectors = () => {
  const CHECK = "M20 6 9 17l-5-5", CROSS = "M18 6 6 18M6 6l12 12";
  const Ico = ({d}) => <svg className="tx-ico" viewBox="0 0 24 24" aria-hidden="true"><path d={d} /></svg>;
  return <figure className="tx tx-fig not-prose" aria-label="Two collectors scrape the same target, with and without attributes of their own">
    <div className="tx-fig-head">
      <p className="tx-fig-title">Two collectors scrape the same counter: 100, 250, 400, 550</p>
      <div className="tx-legend"><span><i className="tx-sw tx-sw-del" />Increase charted</span><span><i className="tx-sw tx-sw-bad" />Counted twice</span></div>
    </div>
    <div className="tx-cols2">
      <div className="tx-lb">
        <div className="tx-lb-h"><b>No collector attributes</b><span className="tx-tag">{Ico({
    d: CHECK
  })}One series</span></div>
        <div className="tx-rows">
          <span className="tx-hd">Scrape</span><span className="tx-hd">Value</span><span className="tx-hd">Collector A reads</span><span className="tx-hd">Collector B reads</span>
          <span className="tx-t">t1</span><span className="tx-num">100</span><span className="tx-slot tx-slot-base">first point, nothing</span><span className="tx-slot">100 − 100 = <span className="tx-out">0</span></span>
          <span className="tx-t">t2</span><span className="tx-num">250</span><span className="tx-slot">250 − 100 = <span className="tx-out">150</span></span><span className="tx-slot">250 − 250 = <span className="tx-out">0</span></span>
          <span className="tx-t">t3</span><span className="tx-num">400</span><span className="tx-slot">400 − 250 = <span className="tx-out">150</span></span><span className="tx-slot">400 − 400 = <span className="tx-out">0</span></span>
          <span className="tx-t">t4</span><span className="tx-num">550</span><span className="tx-slot">550 − 400 = <span className="tx-out">150</span></span><span className="tx-slot">550 − 550 = <span className="tx-out">0</span></span>
        </div>
        <div className="tx-total"><span>150 + 150 + 150</span><span><span className="tx-big">450</span> charted</span></div>
      </div>
      <div className="tx-lb tx-lb-wrong">
        <div className="tx-lb-h"><b>Each collector adds its pod name</b><span className="tx-tag tx-tag-bad">{Ico({
    d: CROSS
  })}Two series</span></div>
        <div className="tx-rows">
          <span className="tx-hd">Scrape</span><span className="tx-hd">Value</span><span className="tx-hd">Series A</span><span className="tx-hd">Series B</span>
          <span className="tx-t">t1</span><span className="tx-num">100</span><span className="tx-slot tx-slot-base">first point, nothing</span><span className="tx-slot tx-slot-base">first point, nothing</span>
          <span className="tx-t">t2</span><span className="tx-num">250</span><span className="tx-slot">250 − 100 = <span className="tx-out">150</span></span><span className="tx-slot">250 − 100 = <span className="tx-out">150</span></span>
          <span className="tx-t">t3</span><span className="tx-num">400</span><span className="tx-slot">400 − 250 = <span className="tx-out">150</span></span><span className="tx-slot">400 − 250 = <span className="tx-out">150</span></span>
          <span className="tx-t">t4</span><span className="tx-num">550</span><span className="tx-slot">550 − 400 = <span className="tx-out">150</span></span><span className="tx-slot">550 − 400 = <span className="tx-out">150</span></span>
        </div>
        <div className="tx-total tx-total-bad"><span>450 + 450</span><span><span className="tx-big">900</span> charted</span></div>
      </div>
    </div>
    <figcaption className="tx-caption">This example assumes both collectors read identical values in timestamp order. One shared series contributes 450; two separate series contribute 900. The first reading of each series is the baseline.</figcaption>
  </figure>;
};

export const TemporalityResets = () => {
  const ARROW = "M5 12h14M13 6l6 6-6 6", CHECK = "M20 6 9 17l-5-5", CROSS = "M18 6 6 18M6 6l12 12", DASH = "M5 12h14";
  const Ico = ({d}) => <svg className="tx-ico" viewBox="0 0 24 24" aria-hidden="true"><path d={d} /></svg>;
  const useWidth = (ref, min) => {
    const [w, setW] = useState(min);
    useEffect(() => {
      if (!ref.current) return undefined;
      const read = () => setW(Math.max(min, Math.floor(ref.current.clientWidth)));
      read();
      const ro = new ResizeObserver(read);
      ro.observe(ref.current);
      return () => ro.disconnect();
    }, []);
    return w;
  };
  const topRounded = (x, y, w, h, r) => {
    r = Math.min(r, w / 2, h);
    return "M" + x + "," + (y + h) + "V" + (y + r) + "Q" + x + "," + y + " " + (x + r) + "," + y + "H" + (x + w - r) + "Q" + (x + w) + "," + y + " " + (x + w) + "," + (y + r) + "V" + (y + h) + "Z";
  };
  const Bars = ({values, max, ticks, xLabels, color, height, empty, title, note}) => {
    const ref = useRef(null);
    const w = useWidth(ref, 240);
    const h = height || 150;
    const L = 34, R = 6, T = 18, B = 22, pw = w - L - R, ph = h - T - B;
    const y = v => T + ph - v / max * ph;
    const n = values.length, sw = pw / n, bw = Math.min(34, sw * 0.56);
    return <div className="tx-chart" ref={ref}>
        <svg width={w} height={h} viewBox={"0 0 " + w + " " + h} aria-hidden="true">
          {ticks.map(tk => <g key={"t" + tk}>
              <line x1={L} x2={w - R} y1={y(tk)} y2={y(tk)} stroke="var(--tx-grid)" strokeWidth="1" />
              <text x={L - 6} y={y(tk) + 4} textAnchor="end" fontSize="10" className="tx-svgtext">{tk}</text>
            </g>)}
          {title ? <text x={L} y={11} fontSize="11" fontWeight="600" className="tx-svgtext tx-svgtext-fg tx-svgsans">{title}</text> : null}
          {values.map((v, i) => {
      const cx = L + sw * i + sw / 2;
      const e = empty && empty[i];
      return <g key={"b" + i}>
                <text x={cx} y={h - 6} textAnchor="middle" fontSize="11" className="tx-svgtext">{xLabels[i]}</text>
                {typeof v === "number" ? <g>
                    <path d={topRounded(cx - bw / 2, y(v), bw, y(0) - y(v), 4)} fill={color}><title>{xLabels[i] + ": " + v}</title></path>
                    <text x={cx} y={y(v) - 5} textAnchor="middle" fontSize="11" fontWeight="500" className="tx-svgtext tx-svgtext-fg">{v}</text>
                    {note && note[i] ? <text x={cx} y={y(v) - 18} textAnchor="middle" fontSize="10" className="tx-svgtext tx-svgsans">{note[i]}</text> : null}
                  </g> : e ? <g>
                    <rect x={cx - bw / 2} y={y(0) - 26} width={bw} height={26} rx="4" fill="none" stroke={e.bad ? "var(--tx-bad)" : "var(--tx-border)"} strokeDasharray="3 3" strokeWidth="1.25" />
                    {e.bad ? <path d={"M" + (cx - 4) + "," + (y(0) - 17) + "l8,8M" + (cx + 4) + "," + (y(0) - 17) + "l-8,8"} stroke="var(--tx-bad)" strokeWidth="1.75" strokeLinecap="round" /> : null}
                    <text x={cx} y={y(0) - 32} textAnchor="middle" fontSize="10" className={e.bad ? "tx-svgsans tx-svgtext-bad" : "tx-svgsans tx-svgtext"}>{e.label}</text>
                  </g> : null}
              </g>;
    })}
          <line x1={L} x2={w - R} y1={y(0)} y2={y(0)} stroke="var(--tx-border)" strokeWidth="1" />
        </svg>
      </div>;
  };
  const S6_IN = [100, 250, 400, 550, 20, 170, 320];
  const S6_OUT = [null, 150, 150, 150, 20, 150, 150];
  const S6_X = ["t1", "t2", "t3", "t4", "t5", "t6", "t7"];
  const ScrapedLine = () => {
    const ref = useRef(null);
    const w = useWidth(ref, 240);
    const h = 170, L = 34, R = 10, T = 22, B = 22, pw = w - L - R, ph = h - T - B, max = 600;
    const sw = pw / 7, x = i => L + sw * i + sw / 2, y = v => T + ph - v / max * ph;
    const seg = (a, b) => {
      let d = "";
      for (let i = a; i <= b; i++) d += (i === a ? "M" : "L") + x(i) + "," + y(S6_IN[i]);
      return d;
    };
    const rx = (x(3) + x(4)) / 2;
    return <div className="tx-chart" ref={ref} role="img" aria-label="Scraped cumulative values: 100, 250, 400, 550, then the app restarts, 20, 170, 320">
        <svg width={w} height={h} viewBox={"0 0 " + w + " " + h} aria-hidden="true">
          {[0, 200, 400, 600].map(tk => <g key={tk}><line x1={L} x2={w - R} y1={y(tk)} y2={y(tk)} stroke="var(--tx-grid)" /><text x={L - 6} y={y(tk) + 4} textAnchor="end" fontSize="10" className="tx-svgtext">{tk}</text></g>)}
          <text x={L} y={11} fontSize="11" fontWeight="600" className="tx-svgtext tx-svgtext-fg tx-svgsans">Scraped</text>
          <line x1={rx} x2={rx} y1={T - 4} y2={T + ph} stroke="var(--tx-muted)" strokeDasharray="3 3" />
          <text x={rx + 5} y={T + 4} textAnchor="start" fontSize="10" className="tx-svgtext tx-svgsans">App restarts</text>
          <path d={seg(0, 3)} fill="none" stroke="var(--tx-cumulative)" strokeWidth="2" strokeLinejoin="round" />
          <path d={seg(4, 6)} fill="none" stroke="var(--tx-cumulative)" strokeWidth="2" strokeLinejoin="round" />
          {S6_IN.map((v, i) => <g key={i}>
              <circle cx={x(i)} cy={y(v)} r="4" fill="var(--tx-cumulative)" stroke="var(--tx-card)" strokeWidth="2"><title>{S6_X[i] + ": " + v}</title></circle>
              <text x={x(i)} y={y(v) - 9} textAnchor="middle" fontSize="11" fontWeight="500" className="tx-svgtext tx-svgtext-fg">{v}</text>
              <text x={x(i)} y={h - 6} textAnchor="middle" fontSize="11" className="tx-svgtext">{S6_X[i]}</text>
            </g>)}
          <line x1={L} x2={w - R} y1={y(0)} y2={y(0)} stroke="var(--tx-border)" />
        </svg>
      </div>;
  };
  return <figure className="tx tx-fig not-prose" aria-label="An application restart between t4 and t5">
    <div className="tx-fig-head">
      <p className="tx-fig-title">An application restart between t4 and t5</p>
      <div className="tx-legend"><span><i className="tx-sw tx-sw-cum" />Scraped, cumulative</span><span><i className="tx-sw tx-sw-del" />Charted, increase</span></div>
    </div>
    {ScrapedLine({})}
    <div style={{
    marginTop: "8px"
  }} role="img" aria-label="Charted increases: nothing at t1, 150, 150, 150, 20 at t5 after the restart, 150, 150">
      {Bars({
    values: S6_OUT,
    max: 200,
    ticks: [0, 100, 200],
    xLabels: S6_X,
    color: "var(--tx-delta)",
    height: 130,
    title: "Charted",
    empty: {
      0: {
        label: "first",
        bad: false
      }
    },
    note: {
      4: "restart"
    }
  })}
    </div>
    <figcaption className="tx-caption">At t5 the counter falls from 550 to 20, below half of the previous value, so the drop is treated as a reset and 20 is charted as the count since then. At t6 it charts 170 − 20 = 150. A smaller drop, say 550 to 400, is treated as zero.</figcaption>
  </figure>;
};

export const TemporalityMemory = () => {
  const MEM = [100, 250, 400, 550];
  const [i, setI] = useState(0);
  const cur = MEM[i], prev = i > 0 ? MEM[i - 1] : null;
  return <figure className="tx tx-fig not-prose" aria-label="Each point minus the previous point of the same series">
      <div className="tx-fig-head">
        <p className="tx-fig-title">Each point minus the previous point of the same series</p>
        <div className="tx-controls"><button type="button" className="tx-btn" onClick={() => setI((i + 1) % MEM.length)}>Next point</button></div>
      </div>
      <div className="tx-mem">
        <div className="tx-mem-h tx-mem-series-h">Series</div><div className="tx-mem-h">Previous</div><div className="tx-mem-h">This point</div><div className="tx-mem-h">Charted</div>
        <div className="tx-mem-series tx-mem-last">http_requests_total{"{"}path="/"{"}"}</div>
        <div className="tx-mem-val tx-mem-last">{prev === null ? <span className="tx-muted">none</span> : prev}</div>
        <div className="tx-mem-val tx-mem-last">{cur}</div>
        <div className="tx-mem-val tx-mem-last">
          {prev === null ? <span><span className="tx-muted">nothing</span><span className="tx-math">first point</span></span> : <span>{cur - prev}<span className="tx-math">{cur} − {prev}</span></span>}
        </div>
      </div>
      <p className="tx-sr-only" aria-live="polite">{prev === null ? "Point " + cur + ". No previous point, so nothing is charted." : "Point " + cur + ". Previous " + prev + ". Charted " + (cur - prev) + "."}</p>
      <figcaption className="tx-caption">Sherlock subtracts the previous reading when you query. Nothing is configured for it and no collector keeps state.</figcaption>
    </figure>;
};

export const TemporalitySources = () => {
  const ARROW = "M5 12h14M13 6l6 6-6 6", CHECK = "M20 6 9 17l-5-5";
  const Ico = ({d}) => <svg className="tx-ico" viewBox="0 0 24 24" aria-hidden="true"><path d={d} /></svg>;
  return <figure className="tx tx-fig not-prose" aria-label="Where cumulative metrics come from, and what to do in each case">
    <div className="tx-lanes">
      <div className="tx-lane">
        <div className="tx-lane-h">OpenTelemetry SDK</div>
        <div className="tx-flow">
          <span className="tx-node">Your service</span>
          <span className="tx-arrow">{Ico({
    d: ARROW
  })}</span>
          <span className="tx-node">SDK exporter</span>
          <span className="tx-arrow">{Ico({
    d: ARROW
  })}</span>
          <span className="tx-tag"><i className="tx-sw tx-sw-cum" />Cumulative by default</span>
        </div>
        <div className="tx-fixrow">
          <span className="tx-lbl">Recommended</span>
          <code className="tx-code">OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta</code>
          <span className="tx-tag"><i className="tx-sw tx-sw-del" />Delta</span>
        </div>
      </div>
      <div className="tx-lane">
        <div className="tx-lane-h">Prometheus scraping</div>
        <div className="tx-flow">
          <span className="tx-node tx-mono">/metrics</span>
          <span className="tx-arrow">{Ico({
    d: ARROW
  })}</span>
          <span className="tx-node">Collector, <span className="tx-mono">prometheus</span> receiver</span>
          <span className="tx-arrow">{Ico({
    d: ARROW
  })}</span>
          <span className="tx-tag"><i className="tx-sw tx-sw-cum" />Cumulative counters and distributions</span>
        </div>
        <div className="tx-fixrow">
          <span className="tx-lbl">To do</span>
          <span className="tx-tag">{Ico({
    d: CHECK
  })}Nothing</span>
          <span className="tx-muted">No temporality setting applies to scraping. Sherlock converts the running totals when you query.</span>
        </div>
      </div>
    </div>
    <figcaption className="tx-caption">Both lanes chart. The SDK lane is worth switching to delta; the scrape lane has nothing to switch and needs no processor.</figcaption>
  </figure>;
};

export const TemporalityStream = () => {
  const IV = [100, 150, 150, 150];
  const CUM = [100, 250, 400, 550];
  const DUR = 2000;
  const useWidth = (ref, min) => {
    const [w, setW] = useState(min);
    useEffect(() => {
      if (!ref.current) return undefined;
      const read = () => setW(Math.max(min, Math.floor(ref.current.clientWidth)));
      read();
      const ro = new ResizeObserver(read);
      ro.observe(ref.current);
      return () => ro.disconnect();
    }, []);
    return w;
  };
  const topRounded = (x, y, w, h, r) => {
    r = Math.min(r, w / 2, h);
    return "M" + x + "," + (y + h) + "V" + (y + r) + "Q" + x + "," + y + " " + (x + r) + "," + y + "H" + (x + w - r) + "Q" + (x + w) + "," + y + " " + (x + w) + "," + (y + r) + "V" + (y + h) + "Z";
  };
  const Bars = ({values, max, ticks, xLabels, color, height, empty, title}) => {
    const ref = useRef(null);
    const w = useWidth(ref, 240);
    const h = height || 150;
    const L = 34, R = 6, T = 18, B = 22, pw = w - L - R, ph = h - T - B;
    const y = v => T + ph - v / max * ph;
    const n = values.length, sw = pw / n, bw = Math.min(34, sw * 0.56);
    return <div className="tx-chart" ref={ref}>
        <svg width={w} height={h} viewBox={"0 0 " + w + " " + h} aria-hidden="true">
          {ticks.map(tk => <g key={"t" + tk}>
              <line x1={L} x2={w - R} y1={y(tk)} y2={y(tk)} stroke="var(--tx-grid)" strokeWidth="1" />
              <text x={L - 6} y={y(tk) + 4} textAnchor="end" fontSize="10" className="tx-svgtext">{tk}</text>
            </g>)}
          {title ? <text x={L} y={11} fontSize="11" fontWeight="600" className="tx-svgtext tx-svgtext-fg tx-svgsans">{title}</text> : null}
          {values.map((v, i) => {
      const cx = L + sw * i + sw / 2;
      const e = empty && empty[i];
      return <g key={"b" + i}>
                <text x={cx} y={h - 6} textAnchor="middle" fontSize="11" className="tx-svgtext">{xLabels[i]}</text>
                {typeof v === "number" ? <g>
                    <path d={topRounded(cx - bw / 2, y(v), bw, y(0) - y(v), 4)} fill={color}><title>{xLabels[i] + ": " + v}</title></path>
                    <text x={cx} y={y(v) - 5} textAnchor="middle" fontSize="11" fontWeight="500" className="tx-svgtext tx-svgtext-fg">{v}</text>
                  </g> : e ? <g>
                    <rect x={cx - bw / 2} y={y(0) - 26} width={bw} height={26} rx="4" fill="none" stroke={e.bad ? "var(--tx-bad)" : "var(--tx-border)"} strokeDasharray="3 3" strokeWidth="1.25" />
                    {e.bad ? <path d={"M" + (cx - 4) + "," + (y(0) - 17) + "l8,8M" + (cx + 4) + "," + (y(0) - 17) + "l-8,8"} stroke="var(--tx-bad)" strokeWidth="1.75" strokeLinecap="round" /> : null}
                    <text x={cx} y={y(0) - 32} textAnchor="middle" fontSize="10" className={e.bad ? "tx-svgsans tx-svgtext-bad" : "tx-svgsans tx-svgtext"}>{e.label}</text>
                  </g> : null}
              </g>;
    })}
          <line x1={L} x2={w - R} y1={y(0)} y2={y(0)} stroke="var(--tx-border)" strokeWidth="1" />
        </svg>
      </div>;
  };
  const Chips = ({arr, done, fresh}) => <div className="tx-chips">
      {arr.map((v, i) => <span key={i} className="tx-chipwrap">
          {i > 0 ? <span className="tx-arr">→</span> : null}
          <span className={"tx-chip" + (i < done ? "" : " tx-chip-empty") + (i === fresh ? " tx-chip-new" : "")}>{i < done ? v : "·"}</span>
        </span>)}
    </div>;
  const [done, setDone] = useState(4);
  const [prog, setProg] = useState(0);
  const [fresh, setFresh] = useState(-1);
  const [playing, setPlaying] = useState(false);
  const [live, setLive] = useState("");
  const [dots, setDots] = useState([]);
  const figRef = useRef(null);
  const trackRef = useRef(null);
  const state = useRef({
    raf: 0,
    last: 0,
    spawned: 0,
    done: 4,
    prog: 0
  });
  const reduced = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  const stop = () => {
    setPlaying(false);
    cancelAnimationFrame(state.current.raf);
  };
  useEffect(() => {
    if (!playing) return undefined;
    const s = state.current;
    s.last = 0;
    const finish = () => {
      setFresh(s.done);
      s.done += 1;
      s.prog = 0;
      s.spawned = 0;
      setDone(s.done);
      setProg(0);
      setLive("Export " + s.done + ": cumulative " + CUM[s.done - 1] + ", delta " + IV[s.done - 1] + ".");
    };
    if (reduced) {
      const t = setInterval(() => {
        finish();
        if (s.done >= 4) {
          clearInterval(t);
          setPlaying(false);
        }
      }, 1100);
      return () => clearInterval(t);
    }
    const tick = ts => {
      const dt = s.last ? ts - s.last : 16;
      s.last = ts;
      s.prog += dt / DUR;
      const need = Math.floor(Math.min(s.prog, 1) * IV[s.done] / 10);
      const track = trackRef.current;
      for (let k = s.spawned; k < need; k++) {
        s.spawned = k + 1;
        if (track) {
          const id = s.done * 100 + s.spawned;
          const top = track.clientHeight * 0.6 - 4 + (Math.random() - 0.5) * 14;
          const travel = track.clientWidth + 20;
          setDots(d => d.concat([{
            id,
            top,
            travel
          }]));
          setTimeout(() => setDots(d => d.filter(x => x.id !== id)), 950);
        }
      }
      if (s.prog >= 1) {
        finish();
        if (s.done >= 4) {
          setPlaying(false);
          return;
        }
      } else {
        setProg(s.prog);
      }
      s.raf = requestAnimationFrame(tick);
    };
    s.raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(s.raf);
  }, [playing]);
  const play = () => {
    if (playing) {
      stop();
      return;
    }
    const s = state.current;
    if (s.done >= 4) {
      s.done = 0;
      s.prog = 0;
      setDone(0);
      setProg(0);
      setFresh(-1);
    }
    setPlaying(true);
  };
  const step = () => {
    stop();
    const s = state.current;
    if (s.done >= 4) {
      s.done = 0;
      s.prog = 0;
      setDone(0);
      setProg(0);
      setFresh(-1);
      setLive("Reset.");
      return;
    }
    setFresh(s.done);
    s.done += 1;
    s.prog = 0;
    setDone(s.done);
    setProg(0);
    setLive("Export " + s.done + ": cumulative " + CUM[s.done - 1] + ", delta " + IV[s.done - 1] + ".");
  };
  const reset = () => {
    stop();
    const s = state.current;
    s.done = 0;
    s.prog = 0;
    setDone(0);
    setProg(0);
    setFresh(-1);
    setLive("Reset. No exports yet.");
  };
  useEffect(() => {
    if (reduced || typeof IntersectionObserver === "undefined" || !figRef.current) return undefined;
    let fired = false;
    const onSeen = entries => {
      const hit = entries.some(en => en.isIntersecting);
      if (hit && !fired) {
        fired = true;
        io.disconnect();
        state.current.done = 4;
        play();
      }
    };
    const io = new IntersectionObserver(onSeen, {
      threshold: 0.5
    });
    io.observe(figRef.current);
    return () => io.disconnect();
  }, []);
  const before = done > 0 ? CUM[done - 1] : 0;
  const inIv = done < 4 ? Math.floor(prog * IV[done]) : 0;
  const total = before + inIv;
  const now = done < 4 ? inIv : prog === 0 && done === 4 ? IV[3] : 0;
  const label = playing ? "Pause" : done >= 4 ? "Replay" : "Play";
  const cv = CUM.map((v, i) => i < done ? v : null);
  const dv = IV.map((v, i) => i < done ? v : null);
  const xl = [1, 2, 3, 4].map(k => "Export " + k);
  return <figure className="tx tx-fig not-prose" ref={figRef} aria-label="One request counter, exported four times, as cumulative and as delta">
      <div className="tx-fig-head">
        <p className="tx-fig-title">One request counter, exported four times</p>
        <div className="tx-controls">
          <button type="button" className="tx-btn tx-btn-primary" onClick={play}>
            <svg className="tx-ico" viewBox="0 0 24 24" aria-hidden="true">
              {playing ? <path d="M8 5h3v14H8zM13 5h3v14h-3z" fill="currentColor" stroke="none" /> : <path d="M7 5v14l11-7z" fill="currentColor" stroke="none" />}
            </svg>
            <span>{label}</span>
          </button>
          <button type="button" className="tx-btn" onClick={step}>Next interval</button>
          <button type="button" className="tx-btn" onClick={reset}>Reset</button>
        </div>
      </div>
      <div className="tx-stream">
        <div className="tx-track" ref={trackRef} aria-hidden="true">
          <span className="tx-track-label">Requests · each dot is 10</span>
          {dots.map(d => <span key={d.id} className="tx-dot" style={{
    top: d.top + "px",
    animationName: "tx-travel-" + Math.min(1200, Math.round(d.travel / 100) * 100)
  }} />)}
        </div>
        <div className="tx-counter" aria-hidden="true">
          <div><div className="tx-k">Counted since start</div><div className="tx-v tx-num">{total}</div></div>
          <div><div className="tx-k">This interval</div><div className="tx-v2 tx-num">{now}</div></div>
        </div>
      </div>
      <div className="tx-intervals" aria-hidden="true">
        {IV.map((v, i) => <div key={i} className="tx-iv">
            <div className="tx-ivbar"><div className="tx-ivfill" style={{
    width: i < done ? "100%" : i === done ? prog * 100 + "%" : "0%"
  }} /></div>
            Interval {i + 1}
          </div>)}
      </div>
      <div className="tx-pair">
        <div className="tx-panel">
          <div className="tx-panel-h"><i className="tx-sw tx-sw-cum" />Cumulative</div>
          <div className="tx-panel-sub">Running total since the process started</div>
          {Chips({
    arr: CUM,
    done,
    fresh
  })}
          {Bars({
    values: cv,
    max: 600,
    ticks: [0, 200, 400, 600],
    xLabels: xl,
    color: "var(--tx-cumulative)",
    height: 150
  })}
        </div>
        <div className="tx-panel">
          <div className="tx-panel-h"><i className="tx-sw tx-sw-del" />Delta</div>
          <div className="tx-panel-sub">Change during each interval</div>
          {Chips({
    arr: IV,
    done,
    fresh
  })}
          {Bars({
    values: dv,
    max: 600,
    ticks: [0, 200, 400, 600],
    xLabels: xl,
    color: "var(--tx-delta)",
    height: 150
  })}
        </div>
      </div>
      <p className="tx-sr-only" aria-live="polite">{live}</p>
      <figcaption className="tx-caption">Both are valid OpenTelemetry and describe the same traffic. The last cumulative value, 550, equals the sum of the delta values, 100 + 150 + 150 + 150.</figcaption>
    </figure>;
};

Sherlock charts counters and histograms whether you export them as **delta** or as **cumulative**. Cumulative running totals are converted into per-interval increases when you query, so you do not need to change your SDK or add a processor to your collector. Delta is still recommended when you control an OpenTelemetry SDK, and the next two sections say why and how. Counters and distributions a collector scrapes from Prometheus endpoints need nothing at all.

## Two ways to report the same counter

A counter is exported once per interval. Cumulative sends the running total since the process started. Delta sends only what changed during the interval.

<TemporalityStream />

Both are valid OpenTelemetry. Most SDKs default to cumulative. Prometheus counters, and the counts and sums of Prometheus histograms and summaries, are always cumulative.

## What Sherlock charts

| Your instrument | Sent as delta | Sent as cumulative |
| - | - | - |
| Counter (monotonic sum) | Charted as sent | Converted to increases, then charted |
| Histogram (explicit buckets) | Charted as sent | Converted bucket by bucket, then charted. `min` and `max` come from delta histograms only. |
| Exponential histogram | Not charted | Not charted |
| Summary (Prometheus quantiles) | Summaries are always cumulative | Count and sum converted; chart `count`, `sum`, `avg`, and `rate`. Percentiles are not offered. |
| UpDownCounter (non-monotonic sum) | Charted as sent | Not charted yet |
| Gauge | Charted as sent. A gauge carries no temporality. | Charted as sent |

If you need percentiles you can aggregate across instances, expose the value as a histogram instead of a summary. The delta preference leaves UpDownCounters cumulative, as the OpenTelemetry specification requires, so a level you want to chart is best sent as a gauge for now.

## Recommended: delta from SDKs

Delta sends the interval values Sherlock charts directly, and interval `min` and `max` of a histogram exist only in delta data. Cumulative metrics chart too. Some cumulative exporters resend exemplars on every export; Sherlock deduplicates repeated copies when you query them.

<TemporalitySources />

Most OpenTelemetry SDKs read this variable on the service that exports metrics:

```sh theme={null}
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta
```

If you configure the exporter in code, set the temporality preference there instead; the environment variable is ignored when code sets it. The Sherlock SDK for Node.js sends delta on its own, and both variants of the [Go setup](/sdk/go/setup) set it. See the [Node.js quickstart](/sdk/nodejs/quickstart) and [OpenTelemetry SDKs and collectors](/send-data/otlp) for the rest of the exporter settings.

Scraped metrics are a different case. Scraping has no temporality setting, and nothing needs converting in the collector: Sherlock converts the supported running totals when you query and charts gauges as sampled values.

## How cumulative is converted

Each point's increase is its value minus the previous point of the same series. A series is one combination of instrumentation scope, resource attributes, scope attributes, and metric attributes.

<TemporalityMemory />

* Sherlock reads cumulative points from ten minutes before the selected window starts. A point with no predecessor in that range is omitted and becomes the baseline for the next point; it is not plotted as zero. A new cumulative series therefore charts from its second point, and delta points chart from their first export.
* A drop below half of the previous value is treated as a reset, and the new value is charted as the count since then. A smaller drop, including a drop to exactly half, counts as zero.
* These are rules about values, not knowledge of what happened. Timestamp skew between two scrapers can produce a backward reading, and a restart is missed when the counter is already back to half of its old value or more by its first point; that point then contributes only the part above the old value.
* For cumulative histograms, the reset decision is made from the observation count and applied to every bucket and to the sum. A point whose bucket boundaries changed without a detected reset is skipped, and intervals with no bucket increases are omitted. `min` and `max` are not available for a cumulative histogram, because lifetime extremes cannot be turned into interval extremes.

<TemporalityResets />

## Running more than one collector

Every process still needs an identity of its own, for example `service.instance.id` or the pod name. Two processes that share every attribute look like one series. Their readings are mixed, so the increases and apparent resets produce wrong totals.

For cumulative counters, histograms, and summaries, redundant scrapes of the same target share one series when their scope name and resource, scope, and metric attributes match, and the repeated readings subtract to zero. Attributes that differ between the collectors, such as each collector's own pod or host name, create separate series and double the totals. Keep the target's identity on the series and keep the scraping collector's identity off it. Delta copies are added, not deduplicated, so send a delta series from one collector only.

<TemporalityCollectors />

Timestamp skew can still inflate the totals. When the two collectors read the counter within a few milliseconds of each other and their clocks disagree, the later reading can carry the earlier timestamp, and the increase is overcounted by however much the counter moved between the two readings. At normal clock skew that is small.

## If you already run the cumulativetodelta processor

You do not need the [`cumulativetodelta`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/cumulativetodeltaprocessor) processor; Sherlock does the conversion. If you already run it, keep each series on one collector instance. The processor keeps the previous value in memory, and two instances converting the same series count the overlap twice. One scraping replica, a DaemonSet in which each collector scrapes only the pods on its node, or replicas behind the [Target Allocator](https://opentelemetry.io/docs/platforms/kubernetes/operator/target-allocator/) all keep a series on one instance. A load-balanced gateway tier that converts already-scraped metrics does not.

The processor is called `cumulativetodelta` on every collector version. From v0.157.0 it is also called `cumulative_to_delta`, and newer collectors warn at startup that the old name is deprecated. Collectors older than v0.157.0 reject the new name.

## Confirm it worked

1. Send some traffic so the instruments record values.
2. Open the metric on the Metrics page. For a request counter, pick `sum` and compare the window total with the traffic you drove. Allow for the omitted first cumulative point, and for the reset and collector limits above. Delta points can chart from their first export.
3. Expect gaps when there is no activity. A synchronous delta counter or histogram with no recorded measurements in an interval exports nothing for it, which is normal. Observable instruments may still report values when there is no new traffic.

If a counter comes out about twice too high and you run more than one collector, check for attributes that separate the duplicate scrapes, and for two collectors converting the same series with the processor. If the total jumps around, check whether several processes share every attribute, and give each one an identity such as `service.instance.id`.

## Related topics

<CardGroup cols={2}>
  <Card title="OpenTelemetry SDKs and collectors" icon="arrow-right-arrow-left" href="/send-data/otlp">
    Endpoint, header, resource attribute, and the collector pipeline.
  </Card>

  <Card title="Metrics" icon="chart-line" href="/explore/metrics">
    Chart a histogram, group by labels, click an exemplar.
  </Card>

  <Card title="Go setup" icon="rocket" href="/sdk/go/setup">
    Delta is set for you in both variants.
  </Card>

  <Card title="Custom metrics in Node.js" icon="chart-line" href="/sdk/nodejs/custom-metrics">
    Histograms, units, and exemplars from the Sherlock SDK.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.