Recordings,
not rows.
MorletDB keeps high-rate sensor recordings (vibration, current, acoustic, seismic) inside PostgreSQL. It stores every sample losslessly, finds similar intervals across the archive, and gives AI agents read-only tools to investigate them, from a Raspberry Pi at the machine to a server.
Give agents signal evidence to reason with.
An investigation agent can search your recording archive before it answers: “Have we seen a signal like this before?” MorletDB supplies matching intervals and their original samples. Your application connects them to analysis tools, maintenance notes or experiment records.
- There was a magnitude 2.6 earthquake offshore Tarapacá on 14 March 2025 at about 21:03 UTC. Find it at station PB01, describe the signal, and find where similar signals occur in the archive. Check the matches against the earthquake catalogue. If a strong match is not in the catalogue, find out whether it is a real earthquake.
- seismo.catalog_events (station=PB01, 20:50–21:15 UTC)1 event: M2.6 offshore Tarapacá, origin 21:02:50.6, P predicted at PB01 at 21:03:10.8
- morletdb.locate_time (2025-03-14T21:03:10Z, station PB01)CX.PB01..HHZ.20250312T010454, sample 24,469,588
- morletdb.find_onsets (2 Hz high-pass)onset 21:03:09.1 (short-term energy 1,822× the background), later phase 21:03:30.5
- morletdb.compare_intervals (against the background around it)+9.4 dB; 86% of the new energy between 2 and 10 Hz
- The event is clear at PB01: P arrives about 21:03:09 and S about 21:03:30, roughly 30 dB above background. Next I'm searching the archive for similar signals.
- morletdb.search_from_time (21:03:09 UTC at PB01, 2 Hz high-pass, top 15)window framed on the onset; 15 hits at all four stations, distances 0.11–0.13
- seismo.catalog_events (×4, one per station)52 catalogued events each, with predicted arrival times
- Eight of the 15 hits line up with catalogued events. The strongest hit, at PB02 on 13 March around 01:58:30, has no catalogue entry, so I'm checking it at the other stations.
- morletdb.find_onsets (×4, 13 Mar 01:57:50–01:59:50)PB01 01:58:22.5 · PB02 01:58:26.5 · PSGCX 01:58:34.1 · PB06 01:58:39.9
- seismo.predict_arrivals (×2 trial locations)20.95°S 68.95°W, 100 km deep: P at PB01 01:58:21, PB02 01:58:25, PSGCX 01:58:34
- morletdb.compare_intervals (PB01, 13 Mar 01:58)+12.9 dB; 72% of the new energy above 10 Hz
8 of the 15 hits line up with catalogued earthquakes, all within a few seconds of the predicted P or S arrival. The other 7 are not in the catalogue.
It is a real earthquake, missing from the EMSC snapshot.
Seen everywhere: it appears at all four stations, which are up to about 350 km apart. A glitch or local noise at one station would not.
A location fits: a trial hypocentre at about 20.95°S 68.95°W, ~100 km deep, origin ≈01:58:05.7 UTC fits P at PB01, PB02 and PSGCX within about 1 s, and S−P at all three measured stations within 0.4 s.
I'd guess it is somewhere around M2–3, but I haven't calibrated that and it is only a guess.
search_from_time existed, framed the search itself and found 1 catalogued earthquake in 15 hits. Full transcripts: this run, the earlier run.- Bearing 1 on this test rig failed at the end of the test with an outer-race fault. From the vibration recordings, find when it first started to change, describe how it changed, and check whether the other three bearings show anything similar. Cite the snapshots you base this on.
- morletdb.coverage (by bearing)984 snapshots per bearing, one every 600 s, 12–19 Feb 2004, no gaps
- Bearing 1 is IMS test 2 (ZA‑2115, 2000 rpm → outer‑race defect frequency BPFO ≈ 236 Hz). Let me trend all four bearings with bands around BPFO and the high-frequency resonance region.
- morletdb.trend (bearing 1, five bands, whole test)200 points; first sustained change: RMS 16 Feb 03:22, 2–5 kHz 16 Feb 05:52, kurtosis 17 Feb 07:22
- morletdb.trend (bearing 1, 15 Feb 20:00 – 16 Feb 08:00)72 points; 2–5 kHz first sustained change 16 Feb 06:52
- morletdb.compare_intervals (16 Feb 05:52 against 15 Feb 21:02)+1.0 dB; 87% of the new energy at 3–5 kHz, none below 1 kHz
- morletdb.compare_intervals (17 Feb 08:12 against 15 Feb 21:02)+6.8 dB; 4–5 kHz +10.4 dB
- morletdb.trend (×3: bearings 2, 3, 4)first changes: bearing 4 on 17 Feb 08:02, bearing 2 on 18 Feb 12:12, bearing 3 on 18 Feb 18:42
- morletdb.compare_intervals (×3: bearings 4, 2, 3 against 15 Feb)bearing 4 +1.3 dB, 82% at 3–5 kHz · bearing 2 +3.6 dB, 76% below 1 kHz · bearing 3 +4.4 dB, 94% below 1 kHz
Bearing 1's vibration first changed at the snapshot of 2004‑02‑16 03:22:39 and stayed raised until the end of the test. The change was a rise in the 3–5 kHz band, mostly 4–5 kHz. None of the other three bearings changed at that time.
A narrow high‑frequency band rising alone is the usual early sign of a local bearing defect.
Bearing 4 has a weak copy of Bearing 1's 3–5 kHz signal that rises and falls at the same times. That is most likely vibration from Bearing 1 carried along the shared shaft and frame, though I haven't confirmed it.
So the data are consistent with the outer‑race finding, but I could not confirm the outer‑race frequency directly.
trend, which was then made compact. Full transcripts: this run, run 1, run 2.- Bearing 1 in test 2 failed with an outer-race fault. Characterise its fault signature, then find out whether the same signature appears on any bearing in test 3, when it first appears there, and whether you can confirm that it is the same kind of fault. Cite the snapshots you base this on.
- morletdb.coverage (by test)test 2: 3,936 recordings; test 3: 4,216 (one snapshot an hour)
- morletdb.trend (test 2, bearing 1)2.5–5 kHz rises from 16 Feb; kurtosis jumps on 17 Feb 07:32
- Test 2 bearing 1's 2.5–5 kHz band starts rising around 2004-02-16 04:02 and jumps on 02-17 07:32. Next I'm checking its envelope spectrum for the computed defect frequencies.
- morletdb.envelope_spectrum (×3: bearing 1 on 12, 16, 17 Feb; outer race 236.4, inner race 296.9, ball 139.9, cage 14.8 Hz)17 Feb: line at 230.5 Hz +36 dB, 2× at 460.9 Hz +31 dB (noise peaks 8 dB); 16 Feb 04:02: +18 dB; 12 Feb: none; inner race, ball and cage at noise level
- Test 2 bearing 1's signature is a 230.5 Hz envelope line plus its 2× harmonic, within 2.5% of the computed outer-race frequency. Next I'm surveying the four test 3 bearings.
- morletdb.search_from_time (test 2 bearing 1 at 17 Feb 07:32 → test 3)15 hits, all bearing 3 or 4 on 16–17 Apr; best B3.20040417T055255, distance 0.20
- morletdb.envelope_spectrum (×4: test 3 on 17 Apr, bearings 3, 4, 1, 2)231.45 Hz: bearing 3 +36 dB, bearing 4 +33 dB, each with its 2×; bearings 1 and 2 no clear line
- On bearing 3 the line is absent at 04-15 19:52 and clear at 04-16 13:52. Next I'm narrowing that window and checking bearing 4 over the same times.
- morletdb.envelope_spectrum (×19: bearings 3 and 4, back through March, then hour by hour on 16 Apr)bearing 3: none at 07:52, +22 dB at 08:52; bearing 4: weak at 07:52, +20 dB at 08:52
- morletdb.compare_intervals (×2: 17 Apr against each bearing's 4 Mar)bearing 3 +9.9 dB, bearing 4 +4.2 dB
Yes, the same signature appears in test 3, first on bearings 3 and 4 together at 2004-04-16 08:52:55, about 41 hours before the test ends. Bearing 3 is the more likely source.
Both tests show the same repetition rate within a bin, the same harmonic pattern, the same resonance band, and no inner-race, rolling-element or cage lines.
The data fits the same kind of fault (outer race), but it can't prove it: that would take a teardown.
- A fast decision model watches this milling machine and decides after every cut whether the cutting insert should be replaced; each decision is logged in MorletDB. Look at the decisions for the insert whose recordings start with c10. Explain the first replace_insert decision from the recordings: which sensors and values drove it, how this cut compares with earlier cuts of the same setting and with the cuts just before it, and whether the decision looks justified or premature. Cite the recordings you base this on.
- morletdb.decisions (insert c10)10 decisions by jev-1.14.0: continue for cuts 1–4 (p 0.31–0.49), replace_insert from cut 5 (p 0.60, threshold 0.60) to cut 10 (p 0.69)
- The first replace is run 5. Its probability is 0.60, exactly at the threshold. Next I'll look at the state the model saw and the current decision state for each sensor.
- morletdb.decisions (cut 5, with the state the model saw)history: 3 sharp cuts of the same setting; largest deviation spindle-vibration kurtosis 208 against a median of 5.3
- morletdb.decision_state (×6: cut 5, each sensor, all history and sharp-only)spindle current (DC) +25% above all 3 sharp cuts; nearest earlier cuts c04r005–007, labelled not_sharp
- morletdb.read_interval (c10r005-vs, the whole cut)flat at 0.26–0.35 V except a burst around samples 1140–1260, up to 0.88 V
- The 3 sharp history runs the model saw are c04r001–003, all from a different insert (case 4) with the same setting. None of them is a c10 run. Now the c10 cuts just before and after:
- morletdb.describe_interval (×10: cuts 1–4 and 6, spindle vibration and current)spindle current mean 4.73, 4.92, 5.55, 6.16 → cut 6 7.37 V; cut 2 had a larger vibration spike (kurtosis 92) and was continue
- A clear pattern is emerging. One more check: the spindle-current level at which the case-4 insert was first labelled not_sharp.
- morletdb.describe_interval (×3: insert c04, cuts 3–5)mean 5.49 V at its last sharp cut, 6.17 V at its first not_sharp cut
Justified, and if anything one cut late. On spindle load, run 5 matches cuts already labelled not_sharp, and run 4 was at the level where the case-4 insert was first labelled not_sharp.
The logged state suggests the wrong reason. Its biggest number is the vibration-kurtosis spike, which is a single transient against a 3-cut history, and a comparable spike in run 2 did not trigger a replace.
There is no measured wear (flank-wear / VB values) for insert c10 in the collection. … It is not ground truth for c10.
“This cycle looks unusual. Have we seen this signal before? Compare earlier runs and bring back the evidence for review.”
The agent calls a bounded SQL tool with a source interval, asset filter and result limit.
Read matching intervals and pass them to a plotting, signal-processing or model tool.
Return recording IDs and sample ranges alongside comparisons so an engineer can inspect the source.
Integration pattern using existing SQL primitives. Detection models, agent orchestration and machine controls are supplied by your application. The bundled MCP server only reads; try it with Claude in the seismic and machine demos. Agent and anomaly-detection effectiveness have not been evaluated; similarity alone does not establish normality, a fault or its cause.
From a similar shape to the original evidence.
Compressed raw chunks preserve the samples you ingest. Read intervals back across chunk boundaries.
Bounded-lag envelope search compares signal structure without requiring exact zero-lag alignment.
Use JSONB metadata filters to narrow your archive by asset, experiment or operating condition.
Use an exact scorer or optional pgvector probes with exact re-ranking. Indexed candidate selection trades recall for latency.
morlet_latest returns every matching sensor’s last samples or newest chunk statistics, at a fixed number of index lookups per recording.
Commit notifications send one pg_notify per committed transaction with the stored sample ranges; rolled-back and replayed writes send nothing.
Declare a synchronous group: its channels are appended in one transaction, read by sample index, and summarized exactly across channels, such as real power.
Exact roll-ups merge chunk summaries, so min, max, mean and RMS over long intervals come from a few rows, bit for bit the same as decoding every sample.
Append. Search. Read.
Store a recording, find similar intervals, and inspect the original samples. The same SQL connection carries you from measurement to result.
-- Append at the recording’s next sample offset.
SELECT morlet_append_chunk(
'public.getting_started', 'run-a', 0,
ARRAY(SELECT sin(i::float8 / 8)
FROM generate_series(0, 8191) AS i ORDER BY i)
);-- Search a stored interval with a metadata filter.
SELECT * FROM morlet_search_envelope_interval(
'public.getting_started', 'run-a', 0, 4096,
'{"asset":"spindle-1"}'::jsonb, 10
);-- Read original samples from an interval.
SELECT * FROM morlet_read_interval(
'public.getting_started', 'run-b', 0, 4096
);Illustrative SQL calls, not a live database. Create the collection and recordings before appending, then explicitly build envelopes before searching. The guide contains the complete runnable sequence.
Works with
Each entry is a documented integration you can run: follow it for the example, its scope and how it was verified (CI, release trial or manual run).
Names are shown as text; listing indicates a documented integration only and does not imply endorsement, partnership or certification. All names and trademarks belong to their owners (notices).
Bring your recordings to PostgreSQL.
Run the prebuilt release image (PostgreSQL 17 with pg_morlet, no compiling), try the Docker walkthrough with synthetic data, or build the extension with Rust and pgrx. PostgreSQL 17, 18 and 19 are supported by the compatibility matrix. Release images and paper benchmarks use 17. For an existing PostgreSQL 17 or 18 server on Debian, install the release’s package (amd64, arm64; installed with apt and tried before each release). PostgreSQL 19 (release candidate 19rc1) builds from source; images and packages for 19 follow 19.0. PostgreSQL 19 compresses large values with lz4 by default; MorletDB pins its sample and envelope columns to pglz, so storage is byte for byte the same as on 17 and 18 (compatibility).
docker run -d --name morletdb -e POSTGRES_USER=morlet -e POSTGRES_PASSWORD=change-me \
-e POSTGRES_DB=morlet -p 127.0.0.1:54329:5432 -v morletdb-data:/var/lib/postgresql/data \
ghcr.io/merkhet-io/morletdb:0.3.14-alpha.1
docker exec morletdb psql -U morlet -d morlet -c "CREATE EXTENSION pg_morlet"The release image is Linux amd64 and arm64 and does not include pgvector (needed only for indexed probe search). Or try the walkthrough with synthetic data:
git clone https://github.com/merkhet-io/morletdb.git
cd morletdb
docker compose run --rm walkthroughRequires Git and Docker Compose v2. The first run builds the extension and uses synthetic recordings. pgvector is optional for indexed probe search and is not included in this walkthrough. Developer alpha.
When your application needs to investigate signals.
Use MorletDB when you have uniformly sampled recordings (single channels, channels at different rates grouped into acquisitions, or synchronous groups sampled at the same instants), want to find similar intervals, and need to inspect the original evidence in its context.
Connect an AI agent through the bundled read-only MCP server, or give it your own SQL tool, to search similar intervals, inspect their samples, and link its analysis to recordings. Combine results with your own maintenance or experiment records.
Use historical retrieval alongside an external anomaly detector to review flagged intervals and collect comparison examples. Detection thresholds and fault labels belong to your application.
Support agents that gather evidence across machine runs before proposing maintenance, requesting a quality check or escalating an unfamiliar event. Operational benefits remain to be validated.
Compare vibration or motor-current intervals with earlier runs, filtered by asset, operating mode or experiment. A target application; fault diagnosis is not validated.
Keep sensor recordings beside test metadata. Retrieve a matching interval and read its original samples for engineering review.
Search seismic envelopes with bounded-lag correlation. This is the domain used in the published retrieval experiments.
Explore similarity in uniformly sampled acoustic recordings from equipment or experiments. A candidate workflow, not evaluated speech or audio understanding.
Your application already uses PostgreSQL, needs metadata filters, and must connect search results to exact source intervals. Search requires compatible sample rates, channels, units and representation settings.
Use cases beyond the evaluated seismic and appliance-current retrieval (N1) are proposed applications. MorletDB does not provide automatic diagnosis, speech recognition, resampling or multichannel similarity search.
How MorletDB looks at a signal.
A fault rarely shows in one reading. It shows as a shape over time, a knock that rings out, and as energy in one frequency band at that moment. In the picture, both similar events rise as peaks in the same band.
- Recording. Samples on a fixed clock: start and rate are stored once, each time follows exactly.tn = t0 + n / fs
- Envelope. The signal’s loudness over time, the magnitude of its analytic signal. Similar events share it even when their oscillations don’t line up.e(t) = |x(t) + i·ℋ{x}(t)|
- Search. Compare envelopes, allowing a small time shift τ, and return the original samples of the best matches.d(q, w) = 1 − max|τ| ≤ L corr(eq, ew(· + τ))
Questions like have we seen this before? are about intervals of a signal; this is the primitive that answers them.
What you don’t have to build.
A general time-series database stores rows of timestamped values. MorletDB stores recordings, so the work around waveforms that you would otherwise build yourself is part of the database.
| Task | General time-series database | MorletDB |
|---|---|---|
| Data model | A row per sample: timestamp, tags, values. You design tables per sampling rate, tag columns and partitions, and every row carries its own time. | A recording: rate, start, channel, unit and metadata, stored once. Samples are addressed by index or by time, and each sample’s time is derived exactly. In B3, time and identity were a third of QuestDB’s native row size. |
| Channels at different rates | A table per rate, or sparse wide tables. Aligning a moment across them takes a range query per table and timestamp arithmetic in the client. | An acquisition groups channels with their own rates, clock offsets and clock uncertainty. One call reads a time interval from every channel, each in its own samples. |
| Resending data | Deduplication often keeps the last write: in B3, an altered resend silently replaced QuestDB’s stored values. | An identical resend changes nothing; an altered resend is refused and the stored samples stay as they were. |
| Operating conditions | Tags repeated on every row, or a separate table joined by time. | Context intervals (program, tool, load, part) become segments with comparable values. A segment is compared with the same unit’s history under the same conditions, optionally only with what came before it. |
| Statistics over hours | Aggregate every row, keep downsampled rollups consistent with the data, or downsample for display (some keep per-block min/max for fast range queries). | Per-chunk summaries give min, max, mean and RMS of one hour at 64 kHz (230 M samples) in 36 ms, bitwise identical to decoding every sample. Optional exact roll-ups answer a year of one 64 kHz channel in milliseconds (17 ms instead of 173 s in a development measurement), bit for bit the same; they can always be rebuilt, and the steward keeps them current. |
| Finding similar signals | Not built in: export windows to a vector database and keep the two in sync. | Similarity search in the same database, with metadata filters and access rights, returning the original samples behind every hit. |
| Your other data | A separate system next to your maintenance and experiment records, unless it runs inside PostgreSQL (TimescaleDB does). | It is PostgreSQL: joins with your own tables, row-level security, transactions, backups and standard drivers. |
| AI agents | Many now ship an MCP server; its tools run SQL over rows, tags and aggregates, and the agent still has to work out your schema and do the signal processing itself. | An MCP server whose tools work on the raw waveforms: locate a time, trend, envelope spectrum, find onsets, compare intervals, search from a moment, each answer citing recording IDs and sample ranges. Typed tools only, no arbitrary SQL, every call in a read-only transaction under your database role. |
| Small machines | Memory needs vary by system; in B3’s memory-cap study (M1), QuestDB, TimescaleDB and IoTDB needed 2 GB to complete the workload, TDengine and CrateDB 1 GB. | A small footprint: B3’s workload completed within 256 MB (M1). Durable ingestion with search features, exact reads, statistics and similarity search ran within 2 CPUs and 512 MB (linux/arm64 container) at the same speed as without limits, with no out-of-memory event (X1). On a Raspberry Pi 3B+ the same workload passed all nine runs (X2). |
This table compares designs, not measurements, except where noted (B3, S1, X1, X2). Time-series databases differ, and several offer some of these: TimescaleDB runs inside PostgreSQL, and many keep roll-ups. For counters, KPIs and dashboards a general time-series database remains the better fit.
Where MorletDB fits.
Most time-series systems are built for rows of readings: metrics, events and analytics. MorletDB is built for the high-rate recordings behind them, and it runs inside PostgreSQL. The quadrant shows what each system is designed for and where it runs, from its own documentation; it is not a ranking. “Inside PostgreSQL” means running in a PostgreSQL server; CrateDB accepts PostgreSQL clients through PostgreSQL’s wire protocol but is a separate database server.
| System | Waveform similarity search | Data model | Altered resend |
|---|---|---|---|
| MorletDB | Built in: similar intervals across recordings, tolerant to time shifts, with the original samples behind every hit | Recordings with rate, channel and unit; acquisitions and context; every sample and its time exact, coded on the signal’s value grid | Refused |
| TimescaleDB | Not built in; pgvector over embeddings you compute | Rows in hypertables, compressed per column | Refused (unique index; B3) |
| QuestDB | Not built in | Rows with nanosecond timestamps | Silently replaces the stored value |
| ClickHouse | Not built in; vector index over embeddings you compute | Columns with configurable codecs | not tested |
| CrateDB | Not built in; vector search over embeddings you compute (up to 2,048 dimensions) | Rows or arrays; millisecond timestamps | Silently ignored |
| Apache IoTDB | Not built in; its optional function library computes DTW or cross-correlation between two series you name | Rows in tables per device group; nanosecond timestamps | Silently replaces the stored value |
| TDengine | Not built in; its AI agent targets forecasting and anomaly detection | A table per device under a supertable with tags; nanosecond timestamps; its float compressor tsz is lossy | Silently replaces the stored value |
| Parquet, HDF5, TDMS files | None | Files per recording or group; exact, but no indexed interval reads across files | — |
Use them together. If you already run CrateDB, TimescaleDB or a historian for plant-wide readings and dashboards, keep it for that. Store the vibration, current and acoustic recordings behind those readings in MorletDB, and link the two by asset and time.
WHERE ANOTHER CHOICE FITS BETTERCapabilities as documented for the versions listed; resend behaviour as measured in the benchmarks, where all measured results are. Names are shown as text and imply no endorsement.
Smaller archives. Faster evidence.
Every result below comes from a protocol frozen before measuring, on real recordings, with every sample read back bit for bit. Each comparison reports its losses in the linked results. Storage is compared like for like: MorletDB’s whole database (161 MiB, of which 149 MiB sample data; 163 MiB with chunk summaries, on by default since 0.3.7-alpha.1) against each database’s data on disk. B3’s headline ingestion rates use asynchronous commits, with the durable figures alongside; the small-machine studies X1 and X2 measure durable ingestion with search features, so their rates are lower by design.
STORAGE AGAINST DATABASES AND CODECS| Against | Where MorletDB is smaller | Evidence |
|---|---|---|
| TimescaleDB 2.30 | 24–35% smaller than its compression on integer columns and 2.3–6.3× smaller on float columns, where Gorilla coding expands noisy values beyond 64 bits per sample | P2 ↗ 4 signals |
| ClickHouse 26.5, TileDB 2.30 | Smaller than their best codec chains on float values for all six test signals; 24–34% smaller than ClickHouse ZSTD(19) on calibrated physical values | B2 ↗ 6 signals |
| ClickHouse 26.10 (ALP, PCO), Pcodec 1.0.4, Parquet ALP | Smaller than ClickHouse’s PCO on 5 of 6 signals and than Pcodec on 4 of 6 (up to 20%); 2.6–7.9× smaller than every ALP variant on calibrated sensor values. Pcodec was smaller on IMS decimals (8%) and Paderborn physical values (5%), ClickHouse PCO on Paderborn physical (3%) | B4 ↗ 6 signals + 22 series |
| Parquet, zstd, ALP files | 1.5–1.6× smaller than the best float codec on calibrated values, and 47% smaller than a Parquet zstd 9 file per sensor group, without giving up indexed interval reads | B1 ↗, B3 ↗ |
The storage advantage comes from coding samples on their value grid (ADC counts, calibrated sensors, fixed decimals), which MorletDB detects automatically, with no calibration step to declare; signals without any value grid compress less well. Codecs at default settings unless stated.
Against time-series databases
The same 101.6 million samples of bearing vibration and motor current, one machine, identical containers, protocols frozen before measuring, every sample read back bit for bit. Each dot is one database on one measure, as a multiple of MorletDB’s figure, measured in one session with the two databases loading alternately; hover a dot for the values, open a tab for every number and its conditions.
How these ratios were measured
Every speed and memory ratio comes from one session in which a released MorletDB and that database ran alternately, with reads from a fresh client process: QuestDB against MorletDB 0.3.7-alpha.1 reading with compressed transfer, as its Python SDK does (B3.16, a browser process used about one core during that session; B3.15 measured smaller margins); CedarDB against 0.3.9-alpha.1 with its default chunk summaries (B3.18), its interval statistics against 0.3.10-alpha.1 (B3.19, level: no statement); InfluxDB 3 Core against 0.3.9-alpha.1 (B3.18); GreptimeDB against 0.3.12-alpha.1 (B3.21); the other databases against 0.3.6-alpha.1 (B3.13) until they are measured against the new release. Storage against MorletDB’s whole database: QuestDB’s native columns (dot) and its Parquet zstd 9 partitions (ring); CrateDB, IoTDB, TDengine (best lossless layout), TimescaleDB (compressed columnstore), CedarDB (its data directory), InfluxDB (its Parquet files after a snapshot) and GreptimeDB (its Parquet files after flush and compaction). Reads and statistics at p50; on QuestDB’s lane interval statistics use chunk summaries, on by default in 0.3.7. Ingestion from one client connection; memory is the smallest memory cap that completes the workload (Study M1: caps of 256 MB, 512 MB, 1 GB, 2 GB and 4 GB, swap off, every read exact, all against 0.3.7-alpha.1 in one session; CedarDB in Study M1b and InfluxDB in Study M1c, both against 0.3.9-alpha.1, GreptimeDB in Study M1d against 0.3.12-alpha.1, where MorletDB passed at 256 MB in one of two runs); caps are powers of two, so these ratios are coarse. Not shown: with 4 or 8 parallel writers MorletDB 0.3.9-alpha.1 ingested 1.6–1.8× faster than QuestDB (B3.17).MorletDB vs QuestDB on real machine dataConfirmed · B3.8, B3.16
101.6 million samples of bearing vibration and motor current (Paderborn bearing dataset, 124 acquisitions at 64 kHz, 4 kHz and 1 Hz). Both databases ran on the same machine in identical containers, loaded the same samples through their recommended client paths, and returned every sample bit for bit. Protocols were frozen before measuring; tuning used a separate development set.
| Measure | MorletDB | QuestDB 10.1 | Result |
|---|---|---|---|
| Data on disk | 163 MiB whole database, with chunk summaries (sample data alone: 149 MiB) | 233 MiB (Parquet zstd 9) 1,163 MiB (native columns) | 30% smaller than QuestDB’s best compression; 7× smaller than its native columns |
| Sample data, no calibration declared | 151 MiB | 233 MiB (Parquet zstd 9) | 35% smaller; the value grid is detected automatically (G1) |
| Short exact read, 4,096 samples | 0.63 / 1.03 ms | 0.86 / 1.66 ms | 1.4× faster at p50, 1.6× at p95 |
| Three channels × 64,000 samples | 3.5 / 5.0 ms | 10.5 / 12.4 ms | 3.0× faster at p50, 2.5× at p95 |
| Short exact read of recent data, 4,096 samples | 0.69 / 1.31 ms | 0.69 / 1.47 ms | No statement: on par at p50, MorletDB 12% faster at p95 |
| Short exact read of older data, 4,096 samples | 0.61 / 0.93 ms | 0.90 / 1.79 ms | 1.5× faster at p50, 1.9× at p95 |
| Interval statistics (min, max, mean, RMS) | 0.53 / 0.59 ms | 2.88 / 5.84 ms | 5.4× faster at p50, 9.8× at p95, from chunk summaries (on by default) |
| Concurrent short reads, 4 clients | 3,974 queries/s | 1,978 queries/s | 2.0× the throughput; p95 latency 1.7 vs 5.3 ms |
| Long exact read, 128,000 samples | 2.7 / 3.8 ms | 8.3 / 10.8 ms | 3.1× faster at p50, 2.8× at p95 |
| Ingestion, 8 parallel writers per system (B3.17) | 22.4 M samples/s | 14.0 M samples/s | 1.6× faster, using 2.1 of QuestDB’s 7.2 cores (durable commits: 23.2 vs 12.8 M, 1.8×) |
| Ingestion, one client connection | 13.8 M samples/s | 8.9 M samples/s | 1.55× faster (durable commits: 13.4 vs 8.1 M, 1.65×) |
| Smallest memory cap that completes the workload (Study M1) | 256 MB | 2 GB | 8× less (caps are powers of two, so coarse) |
| Server memory while ingesting, 16 GB container: the footprint with the documented configuration, not a need | 177 MiB | 2.2 GiB | 13× less |
| Full workflow within 2 CPUs and 512 MB | passes; same speed as unlimited; no out-of-memory event | not measured | MorletDB alone, container limits (X1) |
| Altered retry of stored data | refused | silently replaces the stored value | Stored samples cannot be overwritten by a faulty resend |
Benchmark B3.16, frozen before its first load: the released MorletDB 0.3.7-alpha.1 reading as its Python SDK does (reads of 32,768 samples or more sent as stored chunks and decoded in the client by the SDK’s native decoder; shorter reads raw), the same release with raw transfer, and QuestDB with its documented configuration (WAL tables, deduplication, TIMESTAMP_NS; Parquet conversion for its compressed form), loaded alternately in one session, 18 loads, every sample read back bit for bit, reads from a fresh client process. A browser process used about one core during that session, and QuestDB measured slower than in the quieter B3.15 the same day, so these margins are probably somewhat larger than on a quiet machine; B3.15 measured long reads 2.6× and interval statistics 4.4× / 7.3× faster (its short-read figures carried a harness cost, since fixed). Storage comes from B3.15, QuestDB’s storage from B3.8, parallel writers from B3.17 (MorletDB 0.3.9-alpha.1). Statements follow a fixed rule: at least 10% in all loads, or at both p50 and p95. Ingestion is non-durable unless stated. One dataset, one machine, the client on the same host as the database’s container, where transfer is costly; results on other data, hardware and networks will differ. Full protocol, raw results and losses: B3.16 results ↗ · B3.15 ↗ · G1 grid detection ↗ · X1 small-footprint study ↗ (2-CPU and 512 MB container limits on the same fast cores and SSD; not edge hardware) · frozen protocol ↗.
MorletDB vs CrateDB on the same dataConfirmed · B3.4, B3.13
The same 101.6 million samples, machine and container limits. CrateDB 6.4 ran as a single node through its official Python client in three layouts: one row per sample instant, the same with its best compression, and arrays of 4,096 samples per row, its documented pattern for dense series. Every load of every layout returned every sample bit for bit. CrateDB stores timestamps in milliseconds, so each layout also stored the sample index to keep 64 kHz times exact. Each cell shows CrateDB’s fastest or smallest layout for that measure.
| Measure | MorletDB | CrateDB 6.4 | Result |
|---|---|---|---|
| Whole database on disk | 161 MiB (sample data alone: 149 MiB) | 1,786 MiB (arrays of samples) 5,244 MiB (row per sample) | a tenth of CrateDB’s most compact layout; 32× less than its row layout |
| Short exact read, 4,096 samples | 0.65 / 1.20 ms | 7.2 / 12.3 ms | 11× faster at p50, 10× at p95 |
| Three channels × 64,000 samples | 9.4 / 13.8 ms | 108 / 118 ms | 11× faster at p50, 9× at p95 |
| Long exact read, 128,000 samples | 8.0 / 11.0 ms | 54.3 / 66.7 ms | 7× faster at p50, 6× at p95 |
| Interval statistics (min, max, mean, RMS) | 3.0 / 4.2 ms | 5.1 / 10.7 ms | 1.7× faster at p50, 2.6× at p95 |
| Concurrent short reads, 4 clients | 3,620 queries/s | 498 queries/s | 7× more throughput |
| Ingestion, one client connection | 11.6 M samples/s | 0.68 M samples/s (arrays) 0.33 M samples/s (rows) | about 17× faster than its faster layout |
| Queryable after the write is acknowledged | at commit | after a refresh: up to 13.5 s (arrays), 1–2 s (rows) | new data is searchable and readable immediately |
| Server memory while ingesting | 176 MiB | 6.0–9.1 GiB (8 GB heap, as documented) | configuration-dependent; not a minimum for either |
| Altered retry of stored data | refused | silently ignored | a faulty resend is reported, not dropped |
Benchmark B3.4, frozen before its first load (13 CrateDB loads, all bit-exact; one load that overlapped a system sleep was rerun from a fresh volume, as the protocol requires). Speeds and memory come from B3.13, where the array and row layouts loaded alternately with the released MorletDB 0.3.6-alpha.1 in one session (24 loads, all bit-exact, reads from a fresh client process); B3.4 had compared across sessions and is superseded for speed. Queryable times are B3.4’s. Same statement rule as above. Not tested: parallel clients, CrateDB clusters, its vector search, storage options beyond the frozen layouts. One dataset, one machine. B3.4 results ↗
MorletDB vs Apache IoTDB on the same dataConfirmed · B3.5, B3.13
The same 101.6 million samples, machine and container limits. Apache IoTDB 2.0 ran standalone in its table model, with nanosecond timestamps so every sample’s time is exact, through its official Python client, in two layouts: its defaults (GORILLA encoding, LZ4) and ZSTD. Every load of both layouts returned every sample and time exactly. Each cell shows IoTDB’s faster or smaller layout.
| Measure | MorletDB | IoTDB 2.0 | Result |
|---|---|---|---|
| Whole database on disk | 161 MiB (sample data alone: 149 MiB) | 660 MiB (ZSTD) 770 MiB (defaults: GORILLA, LZ4) | a quarter of IoTDB’s smaller layout; IoTDB’s default is about the raw float64 size |
| Short exact read, 4,096 samples | 0.73 / 1.33 ms | 8.5 / 11.7 ms | 12× faster at p50, 9× at p95 |
| Three channels × 64,000 samples | 11.3 / 17.5 ms | 33.0 / 44.7 ms | 2.9× faster at p50, 2.6× at p95 |
| Long exact read, 128,000 samples | 8.9 / 12.8 ms | 42.4 / 46.9 ms | 4.8× faster at p50, 3.7× at p95 |
| Interval statistics (min, max, mean, RMS) | 2.6 / 3.2 ms | 7.5 / 9.6 ms | 2.8× faster at p50, 3.0× at p95 |
| Concurrent short reads, 4 clients | 3,308 queries/s | 467 queries/s | 7× more throughput |
| Ingestion, one client connection | 13.9 M samples/s | 2.69 M samples/s | about 5.2× faster (durable: 5.7×) |
| Exact time at 64 kHz | derived from start and rate | native nanosecond timestamps | both exact |
| Altered retry of stored data | refused | silently replaces the stored value | a faulty resend is reported, not stored |
Benchmark B3.5, frozen before its first load (12 IoTDB loads, all exact). Speeds come from B3.13, where IoTDB’s table layout loaded alternately with the released MorletDB 0.3.6-alpha.1 in one session (12 loads, all exact, reads from a fresh client process); B3.5 had compared across sessions and is superseded for speed. IoTDB’s reads select the time as an integer, because its Python client converts timestamp columns one value at a time (10× slower); part of its remaining read time is client-side decoding. Not tested: parallel clients, clusters, the tree model. One dataset, one machine. B3.5 results ↗
MorletDB vs TDengine on the same dataConfirmed · B3.6, B3.13
The same 101.6 million samples, machine and container limits. TDengine 3.4 ran as a single node with nanosecond timestamps, through its official Python WebSocket client, in its documented model (a subtable per acquisition under supertables) and three compression layouts: its default (byte-stream split, LZ4), ZSTD at the highest level, and its float compressor tsz. The default and ZSTD layouts returned every sample and time exactly; tsz did not: TDengine documents it as truncating doubles below 10⁻¹⁶, so it is reported as lossy and compared nowhere below. Each cell shows TDengine’s faster or smaller lossless layout.
| Measure | MorletDB | TDengine 3.4 | Result |
|---|---|---|---|
| Data on disk | 161 MiB whole database (sample data alone: 149 MiB) | 567 MiB data files (ZSTD) 1,602 MiB whole volume, with 1 GiB of retained WAL | less than a third of TDengine’s smaller layout |
| Short exact read, 4,096 samples | 0.62 / 1.05 ms | 3.7 / 4.8 ms | 5.9× faster at p50, 4.6× at p95 |
| Three channels × 64,000 samples | 7.4 / 10.2 ms | 36.4 / 41.2 ms | 4.9× faster at p50, 4.0× at p95 |
| Long exact read, 128,000 samples | 6.7 / 9.0 ms | 48.8 / 56.9 ms | 7.3× faster at p50, 6.3× at p95 |
| Interval statistics (min, max, mean, RMS) | 2.5 / 2.6 ms | 3.7 / 10.1 ms | 1.5× faster at p50, 3.9× at p95 |
| Concurrent short reads, 4 clients | 3,998 queries/s | 290 queries/s | 14× more throughput |
| Ingestion, one client connection | 14.3 M samples/s | 4.8 M samples/s | about 3× faster (durable: 2.8×) |
| Lossless float compression | always | default and ZSTD layouts; its float compressor tsz is lossy (error bound 10⁻¹⁶) | every sample exact by design |
| Altered retry of stored data | refused | silently replaces the stored value | a faulty resend is reported, not stored |
Benchmark B3.6, frozen before its first load (12 lossless loads, all exact; the lossy tsz layout stopped after its first load, a recorded deviation). Speeds come from B3.13, where TDengine’s default layout loaded alternately with the released MorletDB 0.3.6-alpha.1 in one session (12 loads, all exact, reads from a fresh client process); B3.6 had compared across sessions and is superseded for speed. TDengine was the fastest-ingesting and least memory-hungry (about 1 GiB) of the IIoT databases tested. Part of its read time is its Python client, which returns rows as Python objects. Not tested: clusters, the enterprise edition, parallel clients. One dataset, one machine. B3.6 results ↗
MorletDB vs TimescaleDB on the same dataConfirmed · B3.7, B3.13
Both are PostgreSQL extensions, here on the same PostgreSQL 17 with default settings, the same data, machine and container limits. TimescaleDB 2.31 stored each group in a hypertable with an integer time dimension in nanoseconds (PostgreSQL timestamps resolve microseconds only, too coarse for 64 kHz), loaded through binary COPY, and was measured twice on the same load: fresh in its rowstore and after compression into its columnstore. Every load returned every sample and time exactly in both states. Each cell shows TimescaleDB’s faster or smaller state.
| Measure | MorletDB | TimescaleDB 2.31 | Result |
|---|---|---|---|
| Data on disk | 161 MiB whole database (sample data alone: 149 MiB) | 815 MiB compressed (columnstore) 6,592 MiB fresh (rowstore) | a fifth of its compressed size; 40× less than fresh data |
| Short exact read, 4,096 samples | 0.65 / 1.13 ms | 1.6 / 2.3 ms | 2.4× faster at p50, 2.1× at p95 |
| Three channels × 64,000 samples | 10.1 / 15.1 ms | 33.8 / 44.4 ms | 3.3× faster at p50, 2.9× at p95 |
| Long exact read, 128,000 samples | 8.2 / 11.4 ms | 40.5 / 52.7 ms | 5.0× faster at p50, 4.6× at p95 |
| Interval statistics (min, max, mean, RMS) | 2.6 / 3.2 ms | 10.2 / 10.7 ms | 3.9× faster at p50, 3.3× at p95 |
| Concurrent short reads, 4 clients | 3,410 queries/s | 966 queries/s | 3.5× more throughput |
| Ingestion, one client connection | 13.8 M samples/s | 0.90 M samples/s, then 27 s to compress | about 15× faster |
| Server memory while ingesting | 177 MiB | 400 MiB | 2.3× less; both with PostgreSQL defaults |
| Resend of stored data | identical: accepted as a no-op; altered: refused | both refused (unique index) | a harmless retry succeeds |
Benchmark B3.7, frozen before its first load (6 loads, all exact in both states). Speeds and memory come from B3.13, where TimescaleDB loaded alternately with the released MorletDB 0.3.6-alpha.1 in one session (12 loads, all exact in both states, reads from a fresh client process); B3.7 had compared across sessions and is superseded for speed. TimescaleDB’s compressed size is about the raw float64 size: its float compression (Gorilla) gains nothing on these noisy calibrated values. Part of its read time is the client converting one row per sample to Python objects, where MorletDB returns an interval as one value. Not tested: continuous aggregates, columnstore writes on ingest, tiered storage. One dataset, one machine. B3.7 results ↗
MorletDB vs CedarDB on the same dataConfirmed · B3.18–B3.20
CedarDB is a fast relational engine with PostgreSQL’s wire protocol, built for analytical and transactional SQL; it leads benchmarks such as ClickBench. Here it stored each group in a table with an integer time column in nanoseconds and a primary key on acquisition and time, loaded through binary COPY like TimescaleDB, with its default settings. Every B3.18 load returned every sample and time exactly.
| Measure | MorletDB | CedarDB | Result |
|---|---|---|---|
| Data on disk | 163 MiB whole database | 4,156 MiB (data directory) | 25× less |
| Short exact read, 4,096 samples | 0.62 / 1.01 ms | 2.7 / 3.5 ms | 4.4× faster at p50, 3.5× at p95 |
| Three channels × 64,000 samples | 7.2 / 9.0 ms | 29.6 / 32.2 ms | 4.1× faster at p50, 3.6× at p95 |
| Long exact read, 128,000 samples | 5.8 / 7.0 ms | 35.8 / 38.0 ms | 6.2× faster at p50, 5.4× at p95 |
| Interval statistics (min, max, mean, RMS) | 0.3.9: 0.68 / 0.75 ms, from chunk summaries 0.3.10: 0.48 / 0.53 ms, computed natively (B3.19) | 0.51 / 0.56 ms 0.52 / 0.58 ms (B3.19) | level with 0.3.10: MorletDB 7–9% faster at p50, under the 10% a statement needs (against 0.3.9: CedarDB 1.35× faster) |
| Concurrent short reads, 4 clients | 4,210 queries/s | 1,126 queries/s | 3.7× more throughput |
| Ingestion, one client connection | 13.4 M samples/s | 2.7 M samples/s | 5.0× faster (durable commits: 13.9 vs 2.6 M, 5.3×) |
| Ingestion with 8 parallel writers (B3.20) | 22.5 M samples/s (durable: 22.1 M) | 5.1 M samples/s (durable: 5.2 M) | 4.4× faster (durable: 4.3×); 4 writers: 4.5× |
| Server memory while ingesting, 16 GB container: the footprint, not a need | 180 MiB | 4.6 GiB | 26× less |
| Smallest memory cap that completes the workload (Study M1b) | 256 MB | 1 GB (did not start at 256 or 512 MB) | 4× less (caps are powers of two, so coarse) |
| Every sample and time read back exactly after a load | 40 of 40 loads | 62 of 63 loads (same sessions and reproduction runs); once a range read returned a row that was never written, while the stored data was correct | fault description ↗, reported to CedarDB (issue #85 ↗) |
| Resend of stored data | identical: accepted as a no-op; altered: refused | both refused (primary key) | a harmless retry succeeds |
Why the gap, and where it is not: CedarDB stores one row per sample, with a timestamp and a key, and returns an interval as one row per sample over the PostgreSQL protocol; MorletDB stores a recording as compressed chunks on the sensor’s value grid and returns an interval as chunks. That explains the storage and the reads; it is the workload, high-rate waveforms, not a slow engine. Where CedarDB does its own kind of work, aggregating over its columns, it computed interval statistics 1.35× faster than the chunk summaries of 0.3.9. Release 0.3.10 computes them natively, and in B3.19 (frozen, three alternating rounds in one session) the two were level: MorletDB 7–9% faster at p50 and 10–11% at p95, short of the 10% at both that a statement needs. In one of CedarDB’s three B3.19 loads, reading one acquisition back returned one wrong row: a time and values that were never written, in place of a stored row. The round was voided. The stored data was correct: a lookup by time returned the row exactly, and the wrong row appeared whenever CedarDB read the acquisition with a table scan restricted by time, also after a restart. It did not recur in 43 further fresh loads, also under heavy machine load, so CedarDB failed the read-back once in 63 loads, including B3.20 (MorletDB: 20 of 20 exact in the benchmark sessions). Fault description ↗ Its ingestion here is one Python client sending rows through COPY, as for TimescaleDB; its server used about 1.3 cores. With 4 or 8 parallel writers (B3.20, frozen, 18 loads, all exact) CedarDB doubled to about 5 M samples/s but did not scale further, using about 3.5 of 8 cores, while MorletDB reached 22 M: the gap widened to 4.3–4.5×. Benchmark B3.18, frozen before its first load, 12 loads, all exact. A macOS maintenance process used more than half a core in some snapshots of that night’s session; the systems alternated, so the ratios hold. One dataset, one machine. B3.18 results ↗ B3.19 results ↗
MorletDB vs InfluxDB 3 Core on the same dataConfirmed · B3.18
InfluxDB is one of the most widely used time-series databases; version 3 stores data as Parquet files. Here each group was one table with the acquisition as a tag, the channels as float fields and nanosecond time, written as line protocol over one HTTP connection with its default settings. New data stays in its write-ahead log until a snapshot writes Parquet, so storage was measured after a snapshot. Every load returned every sample and time exactly.
| Measure | MorletDB | InfluxDB | Result |
|---|---|---|---|
| Data on disk | 163 MiB whole database | 407 MiB (Parquet after a snapshot) | 2.5× less |
| Short exact read, 4,096 samples | 0.62 / 0.99 ms | 9.4 / 10.5 ms | 15× faster at p50, 10.5× at p95 |
| Three channels × 64,000 samples | 7.4 / 9.8 ms | 22.1 / 24.1 ms | 3.0× faster at p50, 2.5× at p95 |
| Long exact read, 128,000 samples | 5.9 / 7.6 ms | 25.6 / 27.7 ms | 4.3× faster at p50, 3.7× at p95 |
| Interval statistics (min, max, mean, RMS) | 0.68 / 0.74 ms, from chunk summaries | 17.5 / 20.5 ms | 26× faster at p50, 28× at p95 |
| Concurrent short reads, 4 clients | 4,298 queries/s | 382 queries/s | 11× more throughput |
| Ingestion, one client connection | 13.9 M samples/s | 0.93 M samples/s | 15× faster (durable writes: 13.6 vs 0.10 M, 133×; see below) |
| Server memory while ingesting, 16 GB container: the footprint, not a need | 180 MiB | 5.3 GiB | 30× less |
| Smallest memory cap that completes the workload (Study M1c) | 256 MB | 4 GB (killed out of memory at 256 MB, 512 MB, 1 GB and 2 GB) | 16× less (caps are powers of two, so coarse) |
| Resend of stored data | identical: accepted as a no-op; altered: refused | both accepted; an altered resend replaces the stored value | stored samples cannot be overwritten by a retry |
Why the gap, and where it is not: InfluxDB stores one row per sample, and every query through its SQL engine cost several milliseconds even for 4,096 samples; that fixed cost dominates short reads and the statistics query, while long reads narrow the gap. Durable writes: by default InfluxDB acknowledges a write once its write-ahead log is persisted, which it does once a second, so one client sending one request after another waited about a second per request (996 ms at p50); several concurrent writers would share a flush and narrow that gap (not measured). Without durable writes a request took 120 ms. Building the line protocol in the client took 26 of the 110 s of a load and is counted, as for every system. Its smallest memory cap is from Study M1c (frozen before its first load, run 2026-11-10): at 2 GB and below InfluxDB was killed out of memory while ingesting; at 4 GB it passed with every read exact, at the same ingestion speed as with 16 GB. Benchmark B3.18, frozen before its first load, 12 loads, all exact; no foreign process used more than half a core during the session. One dataset, one machine. B3.18 InfluxDB results ↗
MorletDB vs GreptimeDB on the same dataConfirmed · B3.21
GreptimeDB describes itself as an open-source observability database: one columnar engine for metrics, logs and traces, storing data as Parquet files on local disk or object storage, and it also runs at the edge. It was measured as InfluxDB was: each group one table with the acquisition as a tag, the channels as float fields and nanosecond time, written as line protocol over one HTTP connection, standalone with its default settings and telemetry switched off. Storage was measured after flushing and compacting every table. Every load returned every sample and time exactly.
| Measure | MorletDB | GreptimeDB | Result |
|---|---|---|---|
| Data on disk | 163 MiB whole database | 199 MiB (Parquet after flush and compaction) | 18% less |
| Whole data volume, write-ahead log included | 362 MiB | 1,102 MiB, of which 901 MiB write-ahead log | 3.0× less |
| Short exact read, 4,096 samples | 0.65 / 1.21 ms | 2.22 / 2.93 ms | 3.4× faster at p50, 2.4× at p95 |
| Long exact read, 128,000 samples | 6.2 / 9.0 ms | 14.2 / 19.2 ms | 2.3× faster at p50, 2.1× at p95 |
| Three channels × 64,000 samples | 8.1 / 12.4 ms | 13.1 / 18.3 ms | 1.6× faster at p50, 1.5× at p95 |
| Interval statistics (min, max, mean, RMS) | 0.45 / 0.50 ms, from chunk summaries | 2.31 / 2.65 ms | 5.2× faster at p50, 5.3× at p95 |
| Concurrent short reads, 4 clients (latency) | 0.85 / 1.68 ms | 3.14 / 4.56 ms | 3.7× faster at p50, 2.7× at p95 |
| Ingestion, one client connection | 14.6 M samples/s | 0.78 M samples/s | 19× faster (durable writes: 14.2 vs 0.79 M, 18×) |
| Smallest memory cap that completes the workload (Study M1d) | 256 MB (one of two runs passed; the other lost a server process during reads) | 1 GB (killed out of memory at 256 MB and 512 MB) | 4× less (caps are powers of two, so coarse) |
| Resend of stored data | identical: accepted as a no-op; altered: refused | identical: no duplicate; altered: replaces the stored value | stored samples cannot be overwritten by a retry |
Why the gap, and where it is not: GreptimeDB's Parquet files are only 18% larger than MorletDB's database, but it kept 901 MiB of write-ahead log on the volume after flush and compaction (it purges the log later on its own schedule). Ingestion is bound by line protocol: building the text in the client took 25 of the 129 s of a load and is counted, as for every system. With durable writes (wal.sync_write on) GreptimeDB ingested as fast as without, so the durable gap is the same. Every query goes through its SQL engine, a fixed cost of about 2 ms that dominates short reads and statistics; long reads narrow the gap. Its smallest memory cap is from Study M1d (frozen before its first load, run 2026-11-13): at 512 MB and below GreptimeDB was killed out of memory while ingesting; at 1 GB it passed with every read exact. Benchmark B3.21, frozen before its first load, 12 loads, all exact. One dataset, one machine. B3.21 results ↗
Exact similarity search, 3.2–3.7× faster, same answersConfirmed · E7
Exact search compares a query with every stored window and returns the guaranteed nearest intervals. From SQL 0.3.3 a fast screen with a rigorous error bound skips windows that cannot be among the nearest, and only the rest are scored exactly. On galleries of seismic event windows from 0.23 to 1.03 million windows, every result list was identical to the previous path, bit for bit, on 100 of 100 queries per gallery.
| Gallery | Exact, screened (0.3.3) | Exact, every window | Result |
|---|---|---|---|
| 234,762 windows | 138 ms | 516 ms | 3.7× faster; median 77 ms (probe search: 64 ms) |
| 504,650 windows | 221 ms | 808 ms | 3.7× faster |
| 1,026,010 windows | 540 ms | 1,928 ms | 3.6× faster |
| Probe search (indexed vector search, pgvector) | 88 / 130 / 237 ms | recall@10 0.990–0.998 | Probes remain 1.6–2.3× faster than exact search at p95 |
Evaluation E7, protocol frozen before timing; all three paths ran in the same session on the same machine, with a desktop workload running (5-minute load about 5), so absolute times are higher than on an idle machine and only the within-session ratios are comparisons. Exact search reads every eligible window, so its time grows with the archive; probe search uses an index. Which one to call: which search to use. Full results: E7 results ↗ · frozen protocol ↗.
Built to run at the edge.
Waveforms are recorded next to the machine, and that is where MorletDB can keep them: on an industrial PC, an edge gateway or a NUC beside the line. It is a PostgreSQL extension with a small footprint, stores every sample exactly, and keeps search, statistics and context on the device, so an engineer or an agent can investigate locally without shipping raw data to the cloud first. To buffer data at the machine, Akhet, a separate collector from the same maintainers, commits MQTT messages and Modbus TCP registers to a local spool before acknowledging them and replays them into MorletDB later; process crashes are tested, power loss not yet. Container images are published for x86-64 and arm64.
To test the low end, we ran it on one of the smallest common single-board computers. In all nine runs, the same image ran the full workflow on a Raspberry Pi 3B+ (4 × Cortex-A53 at 1.4 GHz, 1 GB RAM, microSD card, no fan), within the same 2 CPUs and 512 MB, with the client on the device: durable ingestion with search features, exact reads, statistics and similarity search. Every sample came back bit for bit, and similarity search returned exactly the same results as on the desktop.
Medians of three fanless runs per arm (1.2 GHz at the soft temperature limit): 78,000 samples/s durable ingestion with search features, and 258,000 samples/s as plain lossless storage. That keeps up with about three vibration channels at 25.6 kHz searchable, or about ten stored. Reads of 4,096 samples took 6.4 ms (p50) and a top-10 similarity search 0.6 s; the container peaked at about 120 MiB of its 512 MB (Docker’s measure).
One device, one dataset; not tested for long-term operation or power loss. X2 results ↗
Exact search with long windows is cache-aware since 0.3.8-alpha.1: on the Pi, a query over 1,341 windows of 16,384 samples takes 1.5 s instead of 15.1 s, with bit-identical results; desktops are unchanged (edge latency ↗).
Next to a machine there is no database administrator. tools/steward runs the rebuildable upkeep (envelopes, summaries, roll-ups, verification, statistics) in one budgeted loop, only when the machine is not busy, records every action and shows one plain status. At the disk reserve it holds ingest: appends fail with a clear reason (SQLSTATE 53100) before PostgreSQL runs out of space, and resume when space is free. Its role cannot remove stored samples; changes to stored samples and extension updates are plans that a second person approves.
Tested by filling a real disk on a Raspberry Pi 3B+: new recordings were refused at 295 MB free (300 MB reserve), with no PostgreSQL write error; 14.5 s after space was freed, recording resumed on its own, and every acknowledged sample read back bit for bit.
Not yet tested: power loss and weeks of operation. Disk-fill results ↗ Blog: self-driving Postgres on the edge
IN RESEARCH · STUDY F2 · CONFIRMED ON HELD-OUT DATANot every site can send or keep every sample. For narrow uplinks and long retention we tested a compact approximation: a wavelet transform keeps the largest coefficients, which is where short impacts and onsets live, coded to a fixed byte budget per window. It would travel next to the exact data, never replace it as evidence. In a study frozen before its confirmation data were used, at 0.1 bytes per sample, about a fifteenth of the lossless size:
Results by dataset, and where it falls short
| At 0.1 bytes per sample | Wavelet | SZ3 | Downsampling |
|---|---|---|---|
| Error, bearings, XJTU-SY (lower is better) | 0.70 | 0.97 | 0.76 |
| Error, bearings, Paderborn | 0.55 | 0.72 | 0.63 |
| Error, CNC milling | 0.39 | 0.76 | 0.49 |
| Error, seismic | 0.056 | 0.093 | 0.080 |
| Error, screwdriving cycles | 0.078 | cannot reach | 0.070 |
| Bearing run to failure: faults detected late (of 7) | 0 | 5 (one never) | 2 |
| CNC milling: failed runs separable, AUROC (original 0.682) | 0.671 | 0.740 | 0.716 |
The coder had lower error than SZ3 and ZFP on four or five of the five datasets at every budget from 0.05 to 0.3 bytes per sample, and a heavy-tailed Bayesian prior did not improve it. Where it falls short: with noise filling, an STA/LTA detector still triggered on only 94.8% of the seismic arrivals found on the original (the frozen bar was 95%); on short screwdriving cycles a weak shape statistic weakened further; and thresholding makes envelope spectra peakier, which raised more isolated alarms before a real bearing fault (50 against 1 on the longest run), so alarm thresholds should be calibrated on approximated data. Trend measures such as RMS shift by 7.5% in the plain decode and 0.05% with noise filling; exact per-chunk summaries, which MorletDB already keeps at about 1% of the lossless size, would travel with it.
What it means for the product: a strong long-retention and narrow-uplink tier for vibration from bearings and machine tools, not the only tier for trigger-based event detection or short cycles, where exact data or downsampling is the better choice.
Held-out data: XJTU-SY and Paderborn bearings, PyScrew screwdriving (s02), Chilean seismic stations (2021), two Bosch CNC milling machines. Relative error of the reconstruction, actual coded sizes, Python prototype; two recorded deviations, neither changing a decision. It is not part of a release. F2 results ↗ Report 3 (PDF, draft 0.3) ↗
Tested limits (L1-beta, 96 runs)
| Dimension | Works | Limit or knee |
|---|---|---|
| Sample rate | 1/60 Hz to 64 kHz, all exact and equally fast | above 200 kHz refused; time-to-sample conversion off by one at rates such as 1/60 Hz (fix pending) |
| One recording | up to 109 samples: 7.2 M samples/s in, 24 M out, 4,096-sample reads 0.6–0.7 ms anywhere | statistics over 106 samples 8.9× slower than on short recordings; below ~105 samples per recording fixed overhead dominates |
| Values | counts, decimals, ADC values, floats, constants, denormals, ±1e9, all bit-exact | NaN, ±Inf and values beyond ±1e9 refused |
| Ingest | 16 writers: 21 M samples/s; gaps, altered resends and overlaps refused, identical replays accepted | chunks under ~4,096 samples cost speed and space (64: 5.5× slower) |
| Memory, CPU | 108 samples within 256 MB, no out-of-memory kill; 1 CPU: ingest −28% | read-back −31% at 256 MB |
| Full disk | the failing write is reported to the client | PostgreSQL stops until space is freed: keep the steward's disk guard on |
MorletDB 0.3.9 on one Apple M5 Max, synthetic data, three runs per point, every sample read back bit for bit. L1 results ↗
Edge PCs and gateways usually have more cores and memory than these tests. Not yet tested: weeks of continuous operation, power loss, and specific industrial hardware. X1 container-limit study ↗
A portable format for lossless waveforms.
A MorletDB recording can also be one file: every sample exactly as measured, with its sample rate, unit, channel, start time and metadata, in an ordinary SQLite file that opens on every platform and in practically every language. The file and the server use the same chunk format, so a recording moves between the machine, a laptop, an archive and the central database without conversion and without losing a bit; export a failure case as a file and attach it to a ticket, or load a test run into the plant database. The format is specified and frozen, and a reference reader on Python's standard library reads it without MorletDB. From Python: morletdb.open("line3.morlet"); also a command line and a C library. The embedded edition is experimental; benchmarks and power-loss tests follow before we publish numbers. Read the post.
Claims you can rerun.
A database earns trust with evidence, not adjectives. Everything below is in the repository, so you can check it yourself instead of taking our word for it.
Every study commits its protocol and acceptance criteria before the confirmation run, tunes only on a separate development set, and appends every deviation. Losses are reported next to wins, and raw results are in the repository.
Every load in the B3 benchmark and the X1 and X2 small-machine studies ends by reading every channel back and comparing it bit for bit with the source: 868 channels and 101.6 million samples, each time.
Kani proofs on the chunk codec, nightly fuzzing of decoding and round trips, and frozen golden chunk files that a change may not alter, so stored data stays readable (verification).
An upgraded catalog must equal a fresh installation, including the upgrade from the released version. Release images for x86-64 and arm64 run the quickstart from a clean checkout, and again after a restart, before they are published.
Durable commits, transactions, backups, roles and row-level security come from PostgreSQL itself. An altered resend of stored samples is refused rather than overwriting them.
The MCP server exposes typed signal tools only, with no arbitrary-SQL tool; each call runs in a read-only transaction with a statement timeout, as your database role (MCP). Every search result states what was searched and what was not, so a “no match” says what it covered.
morlet_verify re-reads every chunk (structure, CRC-32C, summaries, gaps) and reports problems without changing data; the steward runs it every week.
MorletDB is alpha software: there is no supported production release or security SLA yet (security policy). Long-term operation and power loss are not yet tested.
Ask the index the right question.
Shifted inner-product probes adapt a standard vector index to bounded-lag waveform retrieval. Candidates are re-ranked with the original scorer.
Measured on 234,622 seismic windows and 100 queries, on one machine with warm caches. Recall measures agreement with the exact scorer, not diagnostic relevance. The sparse grid reached 99.9% recall with exact candidates; ANN search reduced it to 97.7%. Probe structures added approximately 1.0–1.3× the compressed raw-data storage across the two evaluated datasets. Other signal domains remain untested for this retrieval method in the report; study N1 has since tested appliance identification from current. These are the report’s numbers (PostgreSQL 17, held-out set). Since SQL 0.3.3, screened exact search is 3.2–3.7× faster (E7, above).
THREE REPORTS- Report 1 · Recordings, Not RowsLag-tolerant waveform retrieval in PostgreSQL. Version 0.25, released; not peer reviewed.PDF ↗ Supplement ↗ Summary
- Report 2 · Machines with a MemoryEpisodic recall and graded, auditable forgetting for industrial waveforms. Draft 0.2; position and design report.PDF ↗ Summary
- Report 3 · Keeping the Spikes at the EdgeWavelet approximation of machine waveforms at a tenth of lossless size. Draft 0.3; research, not part of a release.PDF ↗ Summary
An archive that lasts, and remembers.
The vision at the top of this page, in more detail: what has shipped toward it, and what comes next.
A recording archive is not a log that only grows. Over years it has to stay fast to search, affordable to keep, and able to answer the question engineers actually ask at the machine: have we seen this before, and what was it? Two lines of thought guide the design.
SCALING: GROW BY YEARS WITHOUT SLOWING DOWNThe argument is set out in report 2, Machines with a Memory (draft) ↗; F1 results in forgetting_f1 ↗.
An open research project.
Built for investigating signal archives. Condition monitoring motivated the work. The report’s retrieval evaluation is seismic; study N1 added appliance identification from current transients, where search matched a current-feature baseline but did not flag unknown appliances. MorletDB is named in honor of Jean Morlet and the project’s initial wavelet hypothesis. Its representations are replaceable, and the current envelope search does not depend on a Morlet transform.