Skip to main content

Benchmarks

This page is pond-ts's one definitive benchmark set — the numbers we judge progress against, refreshed as the library moves. It measures the library against three reference points, in increasing order of difficulty:

  1. pondjs — the predecessor this library rewrites. Answers "was the rewrite worth it."
  2. pandas — the tool a practitioner would otherwise reach for. Answers "is TypeScript a performance sacrifice."
  3. polars — Rust kernels, SIMD, a thread pool. Answers "how far is this from the practical ceiling."

It is deliberately honest in both directions: the tables below include every operation measured, including the ones pond-ts loses. Where it loses, the gap is named, explained, and (where work is planned) linked.

Snapshot: 2026-07-30 · Node 22 · Apple silicon, 10 cores · pondjs 0.9.0 · pandas 2.3.3 · polars 1.36.1 (single-machine medians; treat milliseconds as illustrative and ratios as the signal).

Summary

vs pondjs — faster on all 54 measurable operations, geometric mean 20.7×, plus six transforms that are effectively instant (O(1) column rebinds below timer resolution). The rewrite case is closed.

vs pandas — roughly even, trading blows: pond-ts is ahead on ema (0.59×), mean (0.65×), median / percentile (0.82–0.88×), and behind on the rolling studies (1.1–1.9×). A five-study strategy pass is 1.28× slower than pandas. For a TypeScript library against Cython kernels, even is the headline.

vs polars (single-threaded) — ahead on composite studies (bollinger 0.53×, ema 0.32×, the strategy stack 0.85×), behind 4–9× on whole-column reductions and percentChange, and ~210× behind on raw ingest (a deliberate trade — explained below).

vs polars (all 10 threads) — behind nearly everywhere; the strategy stack runs 3.5× faster on polars-mt. pond-ts has no parallelism today. A measured 2.42× worker-thread path is recorded as [PND-PROCPAR].

The modern workload: 500k bars, resident series

The workload this library is currently driven by: an agent loads years of 1-minute OHLCV bars once, then asks hundreds of questions — studies, strategy stacks, summary facts — with per-query latency as the whole story. All three sides run the same queries with matched conventions (min_periods=period, ddof=0, ewm(adjust=False)); the pandas answers are additionally pinned bar-for-bar by the committed oracle fixtures, so these are comparisons of the same computation, not merely the same label.

vs pandas

Ratio is pond-ts / pandas — below 1.00 means pond-ts is faster.

querypond-tspandasratio
ema(20)2.10 ms3.54 ms0.59×
close.mean()0.19 ms0.29 ms0.65×
close.percentile(95)2.92 ms3.56 ms0.82×
close.median()2.83 ms3.20 ms0.88×
volume.sum() + close.minMax()0.67 ms0.64 ms1.05×
close.minMax()0.48 ms0.43 ms1.11×
percentChange()3.32 ms2.90 ms1.14×
zScore(20)17.69 ms15.27 ms1.16×
sma(20)6.46 ms5.11 ms1.26×
5-study strategy stack63.83 ms50.06 ms1.28×
sma(200)6.60 ms4.94 ms1.34×
bollinger(20)23.03 ms16.50 ms1.40×
close.stdev()2.59 ms1.66 ms1.56×
envelope(20)11.63 ms6.29 ms1.85×
ingest: 6 numeric cols (typed)6.39 ms0.46 ms13.9×

Two structural notes for a fair read: pandas mutates frames in place and tracks missing values as inline NaN; every pond-ts operation returns a new immutable, schema-typed series and consults a validity bitmap. Each side is written the way its users would write it.

vs polars

polars is multi-threaded by default. st (POLARS_MAX_THREADS=1) is the per-core comparison; mt is what polars actually delivers on this 10-core machine. Below 1.00× means pond-ts is faster.

querypond-tspolars stpolars mtvs stvs mt
ema(20)2.03 ms6.26 ms6.31 ms0.32×0.32×
bollinger(20)23.16 ms43.35 ms13.63 ms0.53×1.70×
envelope(20)12.46 ms17.00 ms5.96 ms0.73×2.09×
5-study strategy stack65.80 ms77.71 ms18.87 ms0.85×3.49×
zScore(20)17.68 ms18.48 ms12.90 ms0.96×1.37×
sma(20) / sma(200)~6.7 ms~5.6 ms~5.5 ms1.2×1.2×
close.median()2.86 ms1.64 ms1.92 ms1.75×1.49×
close.mean()0.19 ms0.05 ms0.05 ms3.82×3.74×
volume.sum() + close.minMax()0.67 ms0.16 ms0.16 ms4.15×4.21×
close.percentile(95)2.91 ms0.65 ms0.71 ms4.45×4.11×
close.minMax()0.47 ms0.10 ms0.10 ms4.79×4.89×
percentChange()3.26 ms0.37 ms0.42 ms8.72×7.82×
close.stdev()2.65 ms0.30 ms0.31 ms8.93×8.53×
ingest: 6 numeric cols (typed)6.29 ms0.03 ms0.03 ms~210×~215×

Where pond-ts is behind, and why

Whole-column reductions (1.7–9× vs polars). mean, stdev, minMax, percentile are the simplest kernels in the library and the largest per-core gap. The residue is wide SIMD from native codegen — polars runs 4-wide f64 vector lanes where V8 emits scalar code. It is already half-closed: blocked (8-accumulator) summation shipped in TypeScript and took mean from 8.98× behind to 3.82×. The remaining kernel items are tracked in [PND-KERNEL]. Every one of these queries is also already under 3 ms — behind on ratios, cheap in absolute terms.

percentChange() (8.7× vs polars, 1.14× vs pandas). A pointwise pass that vectorises perfectly in native code. Same SIMD story, smaller stakes.

Rolling means (~1.2× vs both). sma runs a few percent behind two Cython/Rust incremental kernels doing identical O(1)-per-bar work. This is the honest cost of JavaScript on a like-for-like algorithm: real, and small.

Ingest (13.9× vs pandas, ~210× vs polars). All three sides adopt numeric buffers zero-copy; the difference is that pond-ts front-loads validation — a per-cell finiteness scan (which doubles as the allFinite proof that unlocks unguarded reductions later), the non-decreasing-time check that every bisect relies on, and validity derivation — while pandas and polars defer all of it. That is a deliberate trade for the load-once / query-many model: ~6 ms once buys cheaper and safer queries forever after. If you ingest continuously, that trade reads differently, and you should know it exists.

Threads (3.5× on the stack vs polars-mt). Nothing in pond-ts is parallel. polars' own st→mt data shows the win at this scale is inter-operator parallelism only (its sma/ema/reductions gain 1.00× from 10 threads). A Node worker-thread path measuring 2.42× on the real strategy stack — with bit-identical answers — is assessed and recorded as [PND-PROCPAR] (see docs/notes/worker-threads-assessment-2026-07.md).

Where pond-ts is ahead

Sequential recurrences. ema is 2–3× faster than both engines — a recurrence doesn't vectorise, so the columnar walk wins on overhead.

Composite studies, per core. bollinger at 0.53×, the strategy stack at 0.85× vs single-threaded polars: chained multi-output studies are where pond-ts's fused single-pass kernels and cheap immutable construction pay.

Order statistics vs pandas. median / percentile run ahead of pandas (quickselect over a packed column, no full sort).

And the properties that don't show up in milliseconds: every operation returns an immutable, schema-typed snapshot; results are validated bar-for-bar against a pandas oracle; the same code runs in Node and the browser; and the Arrow doors (fromArrow / toArrow) make "hand a column to polars or DuckDB" a zero-copy buffer handoff, so the 9× stdev is reachable when a workload wants it — without pond-ts taking a dependency.

A different axis: concurrent throughput

Every number above is per-query latency — one question, answered as fast as possible. An agent asking several questions at once cares about something the tables cannot show, and pond-ts has recently gained a way to serve that: HostPool in @pond-ts/process, which routes whole requests across worker threads, each holding a long-lived graph.

Measured on the same studies at the same size (500k bars, 16 queries in flight, 8 workers, node packages/process/scripts/perf-pool-studies.mjs):

workloadin-processpooled
16 distinct study queries227 ms87 ms2.60×
16 repeated queries1 ms84 ms0.01×

Both rows matter.

It does not improve a single number in the tables above. One query still runs single-threaded on one worker; the pool adds throughput, not speed. If your agent asks one question at a time, this changes nothing.

And it is catastrophic on repeated questions. In-process, a re-asked question is a memoized hit that returns the same column object for essentially nothing — 1 ms for all sixteen. A pool copies and ships every answer regardless of how cheap it was, and each worker warms its own graph, so they cannot share the hit. Pooling and caching compete rather than compose. Reach for a pool when queries are numerous and mostly distinct; rely on the graph's cache when they repeat.

One finding from that work generalises well beyond the pool: an operation writing a Float64Array rather than a boxed Array ran 482 ms single-threaded where the boxed version needed 632 ms across eight workers. Fixing the allocation beat adding eight cores — and boxing parallelises worse besides, because it contends on memory bandwidth and GC, which is the one resource extra workers cannot add.

vs pondjs: the predecessor

pond-ts is a ground-up TypeScript rewrite of pondjs. Across all shared core operations at three sizes (1k / 4k / 16k events): faster on every one of the 54 measurable benchmarks, geometric mean 20.7×, with six transforms (select / rename at each size) running below the timer's usable resolution (<0.01 ms, reported as "instant" and excluded from the mean).

CategorySpeedup at N=16kKey architectural difference
Aggregation165–453xO(N+B) single-pass bucketing vs O(N×B) Pipeline
Rate~250xSingle columnar walk vs Pipeline materialization
Statistics86–157xQuickselect / Welford on typed arrays vs ImmutableJS
Fill70–86xSingle columnar pass per strategy vs Pipeline/column
Alignment63xForward cursor vs repeated binary search
Construction13xColumnar intake + frozen objects vs ImmutableJS
Chained8xDerived constructors vs per-step Pipeline + collect
Transformsselect/rename instant; collapse 31x; map ~4xColumn-store reshapes vs Pipeline per event
Event access7xArray indexing vs ImmutableJS get()
Serialization4xLightweight columnar representation

The narrowest measurable gaps are map() (~2–5x — both libraries call a user-supplied function per event) and serialization (~4x).

Why it's faster

A columnar store, not per-event Pipelines. pondjs routes every operation — even select() — through a Pipeline that pushes events through observable nodes. pond-ts keeps data in typed-array columns; select and rename are metadata-only column rebinds, and aggregation, fill, rate, and the reducers walk the arrays once.

No ImmutableJS. pondjs wraps event data in ImmutableJS maps and pays for it on every access. pond-ts uses plain frozen objects, materialized lazily from the column store.

Better algorithms. O(N+B) bucketing (vs O(N×B)), O(N) sliding windows, quickselect order statistics (vs full sorts), a forward alignment cursor (vs binary search per point), O(log N) includesKey.

Detailed results

Operation N pondjs (ms) pond-ts (ms) Speedup
new TimeSeries() 1000 0.83 0.28 2.9x
new TimeSeries() 4000 3.99 0.35 11.5x
new TimeSeries() 16000 19.04 1.42 13.4x

aggregate(10s, avg) 1000 1.87 0.09 20.5x
aggregate(1m, sum) 1000 1.23 0.03 37.0x
aggregate(10s, avg+max+min) 1000 2.29 0.09 26.6x
aggregate(10s, avg) 4000 6.50 0.13 48.3x
aggregate(1m, sum) 4000 5.30 0.05 99.8x
aggregate(10s, avg+max+min) 4000 9.36 0.07 132.5x
aggregate(10s, avg) 16000 27.98 0.16 180.1x
aggregate(1m, sum) 16000 21.32 0.05 453.1x
aggregate(10s, avg+max+min) 16000 42.81 0.26 164.9x

rate(value) 1000 1.23 0.08 15.9x
rate(value) 4000 6.41 0.30 21.1x
rate(value) 16000 30.30 0.12 249.3x

fill(hold/pad) 1000 0.84 0.10 8.9x
fill(zero) 1000 0.79 0.05 16.0x
fill(linear) 1000 0.87 0.08 10.8x
fill(hold/pad) 4000 3.78 0.17 22.5x
fill(zero) 4000 3.69 0.05 78.6x
fill(linear) 4000 3.95 0.06 69.2x
fill(hold/pad) 16000 15.56 0.18 86.0x
fill(zero) 16000 15.50 0.18 85.8x
fill(linear) 16000 16.08 0.23 69.8x

select(value) 1000 0.87 <0.01 instant
map(x*2) 1000 0.81 0.41 2.0x
collapse(a+b+c, sum) 1000 1.56 0.18 8.8x
rename(value→measurement) 1000 1.14 <0.01 instant
select(value) 4000 4.84 <0.01 instant
map(x*2) 4000 4.47 0.91 4.9x
collapse(a+b+c, sum) 4000 6.21 0.56 11.1x
rename(value→measurement) 4000 6.02 <0.01 instant
select(value) 16000 20.12 <0.01 instant
map(x*2) 16000 20.32 4.84 4.2x
collapse(a+b+c, sum) 16000 28.20 0.91 31.1x
rename(value→measurement) 16000 26.75 <0.01 instant

align(5s, linear) 1000 1.63 0.16 9.9x
align(5s, linear) 4000 5.97 0.18 33.0x
align(10s, linear) 16000 20.19 0.32 62.6x

at(i).get() full scan 1000 0.27 0.18 1.5x
at(i).get() full scan 4000 0.79 0.20 4.0x
at(i).get() full scan 16000 2.84 0.42 6.7x

toJSON() 1000 0.44 0.19 2.3x
toJSON() 4000 1.85 0.40 4.6x
toJSON() 16000 8.15 2.00 4.1x

map → select 1000 1.78 0.33 5.4x
map → select 4000 9.06 0.91 10.0x
map → select 16000 40.29 5.07 7.9x

median(value) 1000 0.40 0.08 5.2x
stdev(value) 1000 0.33 0.04 8.6x
median(value) 4000 2.17 0.07 30.3x
stdev(value) 4000 1.41 0.02 68.1x
median(value) 16000 12.63 0.08 157.0x
stdev(value) 16000 6.97 0.08 86.2x

The median jump since earlier snapshots (17.7x → 157x at 16k) is the quickselect percentile kernel; aggregate(1m, sum) (81x → 453x) is the columnar-output + in-place bucket reduction work.

Methodology

  • Medians over repeated runs (15–25 samples per query at 500k; medians-of-20 for the pondjs suite), on one otherwise-idle machine.
  • Warm-up is by iteration count, not time. V8's optimising tier is a cliff (~800 iterations at these sizes), not a curve — an under-warmed benchmark reads like a targeted regression in whichever query runs first. The 500k suite warms 1,000 iterations per query.
  • Same answers, not just same labels. The studies are pinned bar-for-bar against committed pandas oracle fixtures (ddof=0, ewm(adjust=False), linear quantile interpolation); polars runs eagerly with ddof passed explicitly. A benchmark of a different computation would be noise.
  • Both sides idiomatic. pandas mutates in place; polars uses its expression API; pond-ts returns immutable typed series. Nobody is hand-tuned into unidiomatic shapes.

Why not TPC-H (PDS-H)?

polars maintains a decision-support benchmark derived from TPC-H's 22 relational queries. We considered adopting it and declined: those queries are multi-table join workloads over a generated warehouse — a dataframe-engine benchmark that a time-series library would either fail (it has no relational joins, on purpose) or answer by becoming what it isn't. What this page takes from PDS-H is its discipline — published queries, declared scale, validated answers — which the pandas oracle already enforces more strictly than a ratio check would.

Reproduce

npm run build --workspaces

# 1. vs pondjs (small-N, 54 operations)
node packages/core/bench/vs-pondjs.cjs

# 2. the 500k agent workload, pond-ts alone (the standing acceptance benchmark)
node packages/financial/scripts/perf-agent-queries.mjs

# 3 + 4. vs pandas and vs polars (needs a Python with pandas + polars)
python -m venv /tmp/pondvenv && /tmp/pondvenv/bin/pip install pandas polars
PYTHON=/tmp/pondvenv/bin/python node packages/financial/scripts/perf-vs-oracle.mjs
PYTHON=/tmp/pondvenv/bin/python node packages/financial/scripts/perf-vs-polars.mjs

# 5. concurrent throughput (the worker pool, on these same studies)
node packages/process/scripts/perf-pool-studies.mjs

Capabilities only in pond-ts

Beyond performance, pond-ts adds functionality pondjs does not have:

  • Live streaming: LiveSeries, LiveView, LiveAggregation, LiveRollingAggregation
  • Live composition: chain filter → diff → fill → aggregate on streaming data
  • Zero-copy Arrow interop both directions: fromArrow (including null-bearing columns) and toArrow
  • filter() as a first-class TimeSeries method (pondjs requires Pipeline)
  • diff(), pctChange(), cumulative(), shift() columnar primitives
  • groupBy() with optional transform callback
  • bfill (backward fill) strategy
  • TypeScript-first schema types that flow through every operation