How Inillucent was made faster than SQLite
On 5 September 2026 the engine was 432% slower than SQLite 3.53.4 on its own benchmark. On 8 October release 2.4.2 was 505% faster on the same benchmark, 276% faster through the API a program calls, and 74% faster when called from the command line, the shell, Python and Node. This report explains the loop we ran, the benchmarks we trusted and why, the changes that moved the numbers most, the advice we took and the advice we did not, and the mistakes in measurement we found on the way.
- Engine, 5 September
- 432% slower
- Engine, 8 October
- 505% faster
- Through the API, 3 to 8 October
- 18% slower to 276% faster
- Called by programs, 5 to 8 October
- 216% slower to 74% faster
- Workloads faster than SQLite, 11 October
- 110 of 116
01The short version
A hill climb is a loop: measure, change one thing, measure again, and keep the change only when the numbers say it helped. The work in September followed that loop, and from 3 October we ran six formal hill climbs, each with a fixed eval, against SQLite on one Windows machine. Every speed figure on this page is a ratio of SQLite’s time to Inillucent’s in the same round, so 2.00x is 100% faster and 0.50x is 100% slower.
- The engine was rewritten. Leaf pages store rows column by column, and the executor works on batches of rows. 432% slower to 206% faster.
- A write statement stopped rebuilding itself on every execution, and the
transactionfamily rose above SQLite. 283% to 326% faster. - A commit became one log append and one sync,
count(*)one addition per batch, and an index build writes its pages once. 255% to 353% faster. - A fix to the measurement. Unpinned, Windows had put Inillucent on the slow efficiency cores and SQLite on the fast ones. Both are now pinned to the same eight performance cores.
- Correlated subqueries stopped growing a 3.2 MB array on every execution. The last graded run: 419% faster.
- Release 2.4.2, after the hill climbs through the API and through how programs call it. 505% faster.
| Date | What changed | What it did |
|---|---|---|
| 5 to 6 September | The engine was rewritten around column leaf pages and a batch executor | 432% slower to 206% faster |
| 20 September | One log append and one sync per commit | 255% to 353% faster |
| 23 September | Both engines pinned to the same fast cores | A measurement fix |
| 26 September | Correlated subqueries stopped allocating | 419% faster, the last graded run |
| 3 to 5 October | Three climbs through the API applications call | 18% slower to 116% faster |
| 5 to 6 October | Two climbs on how programs call it | 216% slower to 64% faster |
| 7 October | A climb on common operations and search | 51.5% faster than release 2.3.4 |
| 8 October | Release 2.4.2 measured for this report | 505% faster on the engine, 276% through the API, 74% called by programs |
02What a hill climb is
Each round changes one thing and is judged by a rule written down before the first measurement. The rule, the workloads and the split into training and held out workloads do not change once the climb starts.
- A workload is held out when the FNV-1a hash of its name is 0 mod 3. Held out workloads are never used to design a change, only to accept one.
- Every change is a general mechanism, such as a set lookup, a cached setup or a lock that is not needed. None is keyed on a workload’s SQL text or its data.
- Builds are run in turn, pass by pass, in one quiet window, so a change in the machine’s speed moves both builds.
- A change is kept when the workloads it targets are faster with their whole 95% interval above 1.00, the training and held out means do not fall by more than 2.5%, processor time and memory do not rise by more than 10% and 3%, the tests pass, and every answer still agrees with SQLite.
- 01Fix the workloads
- 02Fix the train and held out split
- 03Write the keep rule
- 04Measure a baseline
- 01Profile a losing training workload
- 02Change one thing
- 03Measure beside the base
- 04Keep or revert
03The benchmarks
Every benchmark runs both engines in the same rounds. A workload’s score in one round is SQLite’s time over Inillucent’s in that round, so a machine that slows down slows both. A workload’s result is the median of its rounds, and a set’s result is the geometric mean of its workloads, so one very fast workload cannot carry it. The engine headline weights ten families, with the weights fixed in a file before any measurement was taken.
| Benchmark | What it runs | Size |
|---|---|---|
The engine plan, inillucent-fullgate | The engine’s own entry points, in ten weighted families. The published headline | 34 workloads |
The same plan with --api connection | The same workloads through Connection::prepare and Statement::step, which every language driver calls | 34 workloads |
| The hill climb plan | What applications meet and the engine plan does not: rows written by INSERT, tables that churn, queries applications write, edge cases, and checks that every written row is still there | 48 workloads, 28 train and 20 held out |
| The usage benchmark | One process per command, SQL scripts piped into the shell, the Python driver and the npm package, against the sqlite3 shell, Python’s sqlite3 module and Node’s node:sqlite | 25 workloads |
inillucent-searchprobe | 5,000 rows of 384 numbers in a search table: inserts, keyword, vector and hybrid searches | 6 cases |
inillucent-hcprobe | One statement shape at a time, in nanoseconds a call | per shape |
| The nightly run | The long test suites, every release target and the gates, every night at 02:00 | all of them |
04Making the numbers trustworthy
A speed number is only useful when a reader can believe it. Each of these rules came from a number that looked like an answer and was not.
- A time counts only after the answer is checked. Every answer is hashed and compared with SQLite’s on every round. The first climb found two wrong answers this way: a reverse ordered LIMIT 20 after 20,000 inserts returned rows from the middle, and a prepared OFFSET returned the first page every time.
- Both engines run on the same cores. The machine has 8 fast performance cores and 16 slow efficiency cores. The same build read 4.40x unpinned and 4.97x pinned, and nothing in the output said so.
- The machine must be quiet. A busy machine slows SQLite about twice as much as Inillucent, so it makes Inillucent look faster. The gate times SQLite against its own idle speed and grades a run only when SQLite is within 3%.
- Measure the noise first. In the climb of 7 October, two rounds that changed no code the gate runs read 1.003 and 0.999.
- Use medians. One SQLite round sometimes took 20 to 35 times its usual time when its own checkpoint landed in it.
- Design from the training set and accept on the held out set. A change that helps only the workload it was written for shows up as a held out set that did not move.
| A number that looked like an answer | What it was | The fix |
|---|---|---|
scan.aggregate read 2.95 ms one day and 1.76 ms another, same build | The core type each run happened to land on | Pin both engines to the performance cores |
txn.large read 0.09x, far slower than SQLite | SQLite timed 2,000 updates that changed nothing. It also hid a real defect: an update that changed nothing took the most expensive path | Reset the column before the clock; skip a write that changes nothing, 4,067 ns to 867 ns |
read.join had an interval of 2.8x to 6.5x | The bootstrap mixed a 21x workload with a 0.86x one, so it measured the distance between them | Grade a family on one value per round |
| 14 command line workloads could never reach three times SQLite | An empty program takes 13 to 17 ms to start from the benchmark, 11 ms of it Node’s hidden console | Time an empty program every round and report the time above it |
| One threshold made one workload 2.3 times faster and another 2.4 times slower | A 4 MiB log fold moved from one timed statement to the next | Profile each statement on its own |
| A test guard read anywhere from 1.18 to 59.22 for the same code | A stopwatch on a loaded machine | Count log writes, page fetches and compiles instead |
05Through the API applications call
The engine headline calls the engine directly. The path every driver uses was much slower: SELECT 1 once cost 132,884 ns outside a transaction against 1,126 ns inside one, because every statement took and released the file lock. From 3 October the climbs measured through Connection::prepare and Statement::step, on 48 workloads that applications meet.
Processor time for one round of the 48 workloads went from 3,906 ms against SQLite’s 1,820 ms on release 2.1.1, 115% more than SQLite, to 641 ms against 1,945 ms on release 2.4.2, 67% less. Peak memory went from 100.96 MiB to 59.78 MiB.
- Training, 28 workloads
- Held out, 20 workloads
- An OR of two indexed columns reads each index: 588 ms to 0.88 ms. A statement that reads no table takes no file lock. Binary search for long IN lists.
- A correlated IN or EXISTS probes an index. A hash GROUP BY keeps its keys in one table. A reader keeps a packed copy of a written leaf.
- length() counts eight bytes at a time. On Windows the file length comes from one system call that asks for nothing else.
- A read keeps its lock for the next statement, the log length is known without asking Windows, and aggregates read a batch at a time.
06Called the way programs call it
On 5 October we asked whether the engine’s speed reached a person using it. It did not. Release 2.1.5 was 216% slower than SQLite over 25 workloads, and the npm package was 29,270% slower, because every call started a new process. Two climbs fixed it in two days.
The command line stays close to SQLite because starting a program costs both engines the same and is most of a short command. From a terminal an empty program starts in 5.7 ms, and a one row query takes 7.55 ms in Inillucent and 7.95 ms in SQLite.
- Node kept one session process instead of starting one per call: a lookup 10.35 ms to 0.076 ms. The shell stopped rescanning its input: one INSERT of 20,000 rows 5,348.9 ms to 81.8 ms. A Python close stopped doing work it did not owe: 12.1 ms to 1.2 ms.
- Python results arrive as Python objects, and the C library loads into Node directly: a Node lookup to 0.046 ms. Followed Fable’s ranked advice in order.
- The Windows release is built with profile guided optimisation, trained on these workloads.
- The CRC of every page uses the processor’s instruction: 3.93 us to 0.64 us for a 32 KiB page.
- One Python execution is one foreign call, and a read keeps its lock for the next statement: a Python point query 17.25 us to 3.3 us.
07Each way of calling it, before and after
Release 2.1.5 on 5 October against release 2.4.2 on 8 October, each against the same use of SQLite. Every one of the 25 workloads is now faster than SQLite, and 7 of them are three times as fast or more.
08The climb of 7 October, round by round
The goal was 50% faster than release 2.3.4 on common operations: select, join, group by, insert and search. The eval was 82 workloads through the Connection API plus 6 search cases, 56 training and 32 held out. A case’s score is its speedup against 2.3.4. The goal was met at round 10.
Round 6 was the largest single step. Half of an autocommit point read was one system call, asking the open log segment its length to learn whether another process had committed. On Windows the process now holds an opportunistic lock on the segment. While it holds the lock no other handle to the file exists on the machine, so the length is known from this process’s own writes. Another process’s open breaks the lock and waits until this process has stopped trusting its count. An autocommit point read went from 1.74 to 1.91 us to 0.46 to 0.55 us.
- Training, 56 cases
- Held out, 32 cases
- A connection keeps its loaded search index across its own writes: one insert and one search, 51 ms to 0.44 ms. It moved the held out set, where the search cases are.
- A read after a write keeps the read lease, so the next read does not take the lock and read the meta record again: 4.6 us to 1.4 us.
- The log oplock on Windows, Fable’s design: an autocommit point read 1.74 us to 0.46 us. The largest step of the climb.
- Aggregates and GROUP BY keys are computed a batch of rows at a time, Fable’s second piece of advice. The goal was met here.
09The largest single changes
Each bar is one change, measured as the time before over the time after on the workload it was written for. Each of the largest removed work the engine did not need to do: a full scan where two index lookups would do, a list searched from the start for every row, an array of 3.2 MB grown and freed on every execution, a new process for every call.
10Asking Fable for advice
Fable is an AI model that is slower and more expensive to run than the one that did the building. We asked it for design advice where measurement could not answer the question, such as whether a change to the log is safe beside an older release. Every answer was a written document. Fable measured nothing and changed no code. The builders measured each claim and kept what held.
| Date | Question | Advice | What happened |
|---|---|---|---|
| 5 October | How to be faster than SQLite on every usage workload | Ten changes ranked with arithmetic for each. Keep the logical insert record in the log. Defer everything at open except the file handle, the format check and the meta record | Followed in order in round 2: 17 of 25 workloads faster than SQLite, up from 5. One suggested change made no difference |
| 6 October | The last eight workloads slower than SQLite | Twelve ranked changes, two premises of the question corrected, and three decisions only the owner could make | The owner approved them, and every one of the 25 workloads became faster than SQLite. Fable estimated a sync at 1.2 ms; a trace measured 46 us |
| 7 October | How to stop asking the log its length before every statement | Reject both ideas offered. Hold an opportunistic lock on the log segment instead, with the order of steps that makes it correct | Built as round 6. Fable expected under 1.0 us; it measured 0.46 to 0.55 us. The tests found one case the advice had not listed, and it was fixed |
| 7 October | How to reach 1.5 from 1.415 | Four changes ranked by gain and risk, with the arithmetic that 82 workloads need the sum of the logs of their speedups to rise by 4.8 | Changes 1 and 2 became rounds 10 and 11. The goal was met at round 10, so change 4 was not needed |
11What we tried and did not keep
Every climb ends with a table of changes that were measured and reverted, so the next climb does not try them again. A few of them:
| Change | Measured | Why it was not kept |
|---|---|---|
| An index nested loop that reads its inner side into a hash table | app.dashboard 0.83x to 0.45x | A probe costs about 184 ns, and reading a row into the table costs the same |
| A finishing step for the hash of a GROUP BY key | ai.group.total 0.91 | The old hash kept keys inserted in order close together, and the cache liked that |
| x % k by a reciprocal multiply | No change | The cost per row is the calls through the expression tree, not the division |
| 8 KiB pages | The same time as 32 KiB | |
| A larger free list for the library’s allocator | 10,000 Python inserts 13.7 ms to 16.1 to 17.5 ms | A block taken from a long free list is a cold cache line |
| A memory mapped read of the data file | 532 us against 473 us for 64 pages | |
| Making a reader block writers during its lease | Not built | A writer would wait for a busy reader, and readers do not hold writers up |
12Where it stands on 8 October 2026
Release 2.4.2 on the engine plan, the median of three runs of 30 rounds. These runs were not graded: SQLite ran 4.0% to 4.4% slower than its idle reference, which makes the figures read somewhat high. The pale bars are the last graded run, release 1.0.32 on 26 September.
Processor time is 73% less than SQLite’s and peak memory 14% more. Memory is the one measure where SQLite is better, and most of the difference is what a Rust process holds before the engine starts. One workload of the engine plan is slower than SQLite: compiling SELECT 1 on every call, 75% slower. Through the Connection API, which keeps the compiled plan, it is 76% faster.
One regression is open. A DELETE of 40,000 rows by a range of rowids was 4% faster than SQLite after the third climb and is 233% slower in 2.4.2. It slowed down in a release between 2.1.5 and 2.3.4, and the climb of 7 October, which measured against 2.3.4, could not see it. It is filed as a known bug.
13After 8 October: 110 of 116 workloads
An eighth climb set a harder goal than a weighted mean: every one of the 116 workloads the benchmark times, in all three measurements, at least as fast as SQLite. It started from 74 of 116, because release 2.5.2 had added three questions to the file system per statement that cost about 20 us each on Windows. Release 2.5.4 ended it at 110 of 116, and 499% faster on the engine plan in a graded run of 11 October 2026, with SQLite within 2% of its idle reference.
The changes: the file system questions are skipped while the log’s oplock shows no other process has opened it; a range delete unlinks the leaves it empties; a join with a large outer side walks the inner index once; an update keeps one copy of each leaf as its undo record; an INSERT ... SELECT into an empty table collects its rows in a few large buffers; and a WHERE of integer comparisons is tested a batch of rows at a time. One change was to the benchmark: its Connection arm compiled a statement inside the clock where SQLite’s arm compiles before it, and now compiles first.
Six workloads are not faster. A range DELETE pays for folding the log into the database file, which lands on it in the benchmark’s fixed order; on its own it is level with SQLite. SELECT 1 compiled from text on every call through the engine’s own API costs more than SQLite’s compile; through the Connection API it is 66% faster. A join over an index range and an autocommit update are within 6% of SQLite, where the same index probes and one disk sync per commit decide the time.
| Measurement | Release 2.5.2 | Release 2.5.4 |
|---|---|---|
| The engine plan through the engine API | 31 of 34 | 31 of 34 |
| The engine plan through the Connection API | 17 of 34 | 32 of 34 |
| 48 common and edge case workloads through the Connection API | 26 of 48 | 47 of 48 |
14What we learned
- Fix the eval before the first measurement. A weighting or a threshold chosen after the results are in chooses the answer.
- Check the answer before trusting the time.
- Measure the noise before judging a change, and use medians when one sample can be 20 times the others.
- Run both engines on the same cores on a quiet machine, and say in the output when it was not quiet.
- When a timing will not hold still, count something that does: compactions, log writes, page fetches, compiles.
- Profile before changing, and profile each statement on its own.
- Measure the way people use the product as well as the engine. The engine was 419% faster while a person calling it from Node waited 29,270% longer than with SQLite.
- Report a floor no program can go under as a floor. Starting an empty program takes longer than three times SQLite’s speed would allow on 14 command line workloads.
- Ask a stronger model for design advice, and measure what it says. Its estimates were wrong twice, and its designs held every time they were built.
- Write down every rejected change with its numbers, so nobody tries it again.
15Limits of this report
- Every figure is from one Windows 11 machine with an Intel Core Ultra 9 285. A Linux run on 23 September measured 53% faster where Windows measured 279% on the same build, and has not been repeated since.
- The newest engine runs were not graded, because the machine was not quiet enough.
- The 5 September figure is computed from that day’s ten family ratios and weights. No run printed it.
- The hill climb workloads were written by the people improving the engine. The held out set guards against tuning to them, and the engine plan’s weights were fixed before any measurement.