Chart · milliseconds · public data

load ClickBench · 1M, warm.

neo 96.0 ms in the public-data run. Closest: Polars at 1.97×. Farthest: pandas at 61.2×.

—

neo crosses once

neo

TAKE THIS CHART WITH YOU

Embed anywhere.

<iframe src="https://n0.is/embed/chart/clickbench-1m" title="load ClickBench · 1M, warm" width="100%" height="520" loading="lazy" sandbox="allow-scripts" referrerpolicy="no-referrer" style="border:0;border-radius:12px"></iframe>

Every engine's multiple of neo

  1. neo
    1×
  2. Polars
    1.97× · 189 vs 96.0
  3. Arrow
    3.89× · 373 vs 96.0
  4. DuckDB
    15.0× · 1,438 vs 96.0
  5. pandas
    61.2× · 5,876 vs 96.0

Bars to scale, from zero to the slowest engine. The gray pair is the engine's measurement and neo's in the same run, in ms.

What this measures

Read ClickBench · 1M from its path to a table in memory: 1,000,000 rows by 105 columns, 842 MB of web hits. Every engine in a fresh process each round, the file in the OS cache, each engine's start-up done before the clock; warm is the median of the loads after the first. neo's table is text, with no types, no nulls and no UTF-8 check, so the rows as text are the like-for-like ones.

What neo is held to

The same loader the app uses, on a file this project did not make, checked column by column and row by row against DuckDB's reading of it before any time is taken.

Each side, this kernel only

Polars1.97× neo · neo vs Polars

Polars reading the public files · neon-cpp/bench/lang/python/standards.py:79
def load_polars(path, mode, loads, opt=None):
    import polars as pl
    kw = {}
    if opt and "infer" in opt: kw = {"infer_schema_length": opt["infer"]}
    if opt and "overrides" in opt: kw = {"schema_overrides": {c: (pl.Float64 if t == "float" else pl.String) for c, t in opt["overrides"].items()}}
    pl.read_csv(io.BytesIO(b"a,b\n1,x\n2,y\n"))  # untimed: the reader's own start-up (its thread pool), on three lines that are not the file
    out, rows, cols = [], 0, 0
    for _ in range(loads):
        t0 = time.perf_counter()
        df = pl.read_csv(path, infer_schema=False) if mode == "text" else pl.read_csv(path, **kw)
        out.append(ms_since(t0))
        rows, cols = df.height, df.width
        del df
    return pl.__version__, out, rows, cols, pl.thread_pool_size()

Arrow3.89× neo · neo vs Arrow

Arrow reading the public files · neon-cpp/bench/lang/python/standards.py:95
def load_arrow(path, mode, loads, opt=None):
    import pyarrow as pa
    import pyarrow.csv as pc
    convert = pc.ConvertOptions(column_types={n: pa.string() for n in header_names(path)}) if mode == "text" else pc.ConvertOptions()
    parse = pc.ParseOptions(newlines_in_values=True) if opt else pc.ParseOptions()
    pc.read_csv(io.BytesIO(b"a,b\n1,x\n2,y\n"))  # untimed: the reader's own start-up (its thread pools), on three lines that are not the file
    out, rows, cols = [], 0, 0
    for _ in range(loads):
        t0 = time.perf_counter()
        t = pc.read_csv(path, convert_options=convert, parse_options=parse)
        out.append(ms_since(t0))
        rows, cols = t.num_rows, t.num_columns
        del t
    return pa.__version__, out, rows, cols, pa.cpu_count()

DuckDB15.0× neo · neo vs DuckDB

DuckDB reading the public files · neon-cpp/bench/lang/python/standards.py:40
def load_duckdb(path, mode, loads, opt=None):
    import duckdb
    con = duckdb.connect()
    con.execute("SELECT 42").fetchall()  # untimed: what the binding sets up on its first query (it imports pandas) is not CSV loading
    opts = ", all_varchar=true" if mode == "text" else ""
    out, rows, cols = [], 0, 0
    for _ in range(loads):
        con.execute("DROP TABLE IF EXISTS t")
        t0 = time.perf_counter()
        con.execute(f"CREATE TABLE t AS SELECT * FROM read_csv(?, header=true{opts})", [path])
        out.append(ms_since(t0))
        rows = con.execute("SELECT count(*) FROM t").fetchone()[0]
        cols = len(con.execute("SELECT * FROM t LIMIT 0").description)
    threads = con.execute("SELECT current_setting('threads')").fetchone()[0]
    return duckdb.__version__, out, rows, cols, int(threads)

pandas61.2× neo · neo vs pandas

pandas reading the public files · neon-cpp/bench/lang/python/standards.py:111
def load_pandas(path, mode, loads, opt=None):
    import pandas as pd
    pd.read_csv(io.BytesIO(b"a,b\n1,x\n2,y\n"), engine="pyarrow")  # untimed: the reader's own start-up
    out, rows, cols = [], 0, 0
    for _ in range(loads):
        t0 = time.perf_counter()
        # engine="pyarrow": pandas' multithreaded reader, one keyword from the default (its C engine is single-threaded:
        # 1.3-1.7 s on worldcitiespop where this takes tens of ms — bench/results/standards.jsonl, the records of 09:02-09:22)
        eng = (opt or {}).get("engine", "pyarrow")
        df = pd.read_csv(path, dtype=str, keep_default_na=False, engine=eng) if mode == "text" else pd.read_csv(path, engine=eng)
        out.append(ms_since(t0))
        rows, cols = df.shape
        del df
    import pyarrow as pa
    return pd.__version__, out, rows, cols, (pa.cpu_count() if (opt or {}).get("engine", "pyarrow") == "pyarrow" else 1)

neo · confidential

neo's kernels, the ports written in-house to beat them, and the techniques behind both stay with the engine. Nothing about how the numbers were made is withheld: the method, the checks every engine had to pass, every number and every place neo is behind are on this page. The engine itself we keep confidential, because we don't want it getting into the wrong hands. The binaries run on request and reproduce every number here.

Measured on Apple M5 Max, 18 cores, 26.4.1, 2026-10-11. Every number here is also on the comparison page of its engine, with how it was taken and the caveats.

All evalsEvery comparisonStart a conversation