morletdb
Open source · alpha · PostgreSQL extension

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.

MISSIONKeep every machine recording exact, and make what happened in it searchable.
VISIONMachines with a memory: every past event, with its context and outcome, one search away.
Illustrative waveform with two highlighted bursts
SIGNAL / AMPLITUDE OVER TIMEILLUSTRATIVE · NOT MEASURED DATA
Primary use case / AI

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.

claude · morletdb + seismo MCP · seismic demo
  1. 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.
  2. 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
  3. morletdb.locate_time (2025-03-14T21:03:10Z, station PB01)CX.PB01..HHZ.20250312T010454, sample 24,469,588
  4. 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
  5. morletdb.compare_intervals (against the background around it)+9.4 dB; 86% of the new energy between 2 and 10 Hz
  6. 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.
  7. 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
  8. seismo.catalog_events (×4, one per station)52 catalogued events each, with predicted arrival times
  9. 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.
  10. 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
  11. 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
  12. morletdb.compare_intervals (PB01, 13 Mar 01:58)+12.9 dB; 72% of the new energy above 10 Hz
  13. 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.

A real Claude Code session (Opus 5.5) on the agent demo: one unattended run, 22 tool calls in 1 min 31 s, condensed here. Claude’s words are verbatim; tool results are shortened. An earlier run, before search_from_time existed, framed the search itself and found 1 catalogued earthquake in 15 hits. Full transcripts: this run, the earlier run.
EXAMPLE / FACTORY INVESTIGATION AGENT
“This cycle looks unusual. Have we seen this signal before? Compare earlier runs and bring back the evidence for review.”
01 / RETRIEVESearch the relevant history.

The agent calls a bounded SQL tool with a source interval, asset filter and result limit.

02 / INSPECTBring samples into analysis.

Read matching intervals and pass them to a plotting, signal-processing or model tool.

03 / EXPLAINLink the answer to its evidence.

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.

Built around signals

From a similar shape to the original evidence.

Lossless recording storage

Compressed raw chunks preserve the samples you ingest. Read intervals back across chunk boundaries.

Search with room for a time shift

Bounded-lag envelope search compares signal structure without requiring exact zero-lag alignment.

Keep context in the query

Use JSONB metadata filters to narrow your archive by asset, experiment or operating condition.

Choose exact or indexed retrieval

Use an exact scorer or optional pgvector probes with exact re-ranking. Indexed candidate selection trades recall for latency.

Newest data in one call

morlet_latest returns every matching sensor’s last samples or newest chunk statistics, at a fixed number of index lookups per recording.

React to new data

Commit notifications send one pg_notify per committed transaction with the stored sample ranges; rolled-back and replayed writes send nothing.

Channels sampled together

Declare a synchronous group: its channels are appended in one transaction, read by sample index, and summarized exactly across channels, such as real power.

Statistics over years

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.

The workflow

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)
);

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.

Ecosystem

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).

Get started

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 walkthrough

Requires 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 to use it

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.

PRIMARY / AGENTIC INVESTIGATIONRetrieve signal history before answering.

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.

ANOMALY WORKFLOWSPut an unusual signal in context.

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.

AUTOMATED TRIAGEGive automated decisions a signal history.

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.

CONDITION MONITORINGRevisit a suspicious machine cycle.

Compare vibration or motor-current intervals with earlier runs, filtered by asset, operating mode or experiment. A target application; fault diagnosis is not validated.

TEST & MEASUREMENTFind a familiar transient in your test archive.

Keep sensor recordings beside test metadata. Retrieve a matching interval and read its original samples for engineering review.

SEISMIC RESEARCHFind event-shaped signals despite small time shifts.

Search seismic envelopes with bounded-lag correlation. This is the domain used in the published retrieval experiments.

ACOUSTIC INVESTIGATIONCompare recurring sounds and bursts.

Explore similarity in uniformly sampled acoustic recordings from equipment or experiments. A candidate workflow, not evaluated speech or audio understanding.

A good fit if…

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.

Theory

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.

qwtime →frequencyx(t) and its envelope e(t)|X(f)|height = energy at each time and frequency
  1. Recording. Samples on a fixed clock: start and rate are stored once, each time follows exactly.tn = t0 + n / fs
  2. 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)|
  3. 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.

Recordings, not rows

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.

TaskGeneral time-series databaseMorletDB
Data modelA 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 ratesA 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 dataDeduplication 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 conditionsTags 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 hoursAggregate 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 signalsNot 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 dataA 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 agentsMany 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 machinesMemory 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.

Landscape

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.

SystemWaveform similarity searchData modelAltered resend
MorletDBBuilt in: similar intervals across recordings, tolerant to time shifts, with the original samples behind every hitRecordings with rate, channel and unit; acquisitions and context; every sample and its time exact, coded on the signal’s value gridRefused
TimescaleDBNot built in; pgvector over embeddings you computeRows in hypertables, compressed per columnRefused (unique index; B3)
QuestDBNot built inRows with nanosecond timestampsSilently replaces the stored value
ClickHouseNot built in; vector index over embeddings you computeColumns with configurable codecsnot tested
CrateDBNot built in; vector search over embeddings you compute (up to 2,048 dimensions)Rows or arrays; millisecond timestampsSilently ignored
Apache IoTDBNot built in; its optional function library computes DTW or cross-correlation between two series you nameRows in tables per device group; nanosecond timestampsSilently replaces the stored value
TDengineNot built in; its AI agent targets forecasting and anomaly detectionA table per device under a supertable with tags; nanosecond timestamps; its float compressor tsz is lossySilently replaces the stored value
Parquet, HDF5, TDMS filesNoneFiles 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 BETTER
Floats without a value gridMorletDB’s storage advantage comes from the digitizer grid inside calibrated values. On five public float series without one it needed 2.2–4.0× the space of ClickHouse’s best codec, and on data that lies almost, but not exactly, on a grid Pcodec and ClickHouse’s PCO codec were 3–8% smaller (Benchmarks; a pco-based fix was tested and failed, B5).
Searching signals above 200 kHzRecordings up to 100 MHz (acoustic emission, ultrasound) are stored, read and summarized losslessly, but search by shape covers 1 Hz to 200 kHz; one writer keeps up with a continuous channel of a few MHz (#342).
Archiving raw integer counts as filesFLAC files are as small or slightly smaller on most signals.
Scalar metrics and dashboardsA general time-series database or an industrial data platform such as CrateDB fits counters and KPIs better; MorletDB is built for waveforms.

Capabilities 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.

Benchmarks

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
AgainstWhere MorletDB is smallerEvidence
TimescaleDB 2.3024–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 sampleP2 ↗
4 signals
ClickHouse 26.5, TileDB 2.30Smaller than their best codec chains on float values for all six test signals; 24–34% smaller than ClickHouse ZSTD(19) on calibrated physical valuesB2 ↗
6 signals
ClickHouse 26.10 (ALP, PCO), Pcodec 1.0.4, Parquet ALPSmaller 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 files1.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 readsB1 ↗, 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.

Log scale. 1× is MorletDB; to the right MorletDB is better, to the left the other database. The tables in the tabs below list every value.QuestDB 10.1CrateDB 6.4IoTDB 2.0TDengine 3.4TimescaleDB 2.31CedarDB 2026-11InfluxDB 3.12GreptimeDB 1.3Hollow ring: QuestDB as Parquet zstd 9 (data on disk) · each dot’s MorletDB release is in its tooltip0.5×1×2×5×10×20×50×MorletDB = 1×MorletDB better →← other betterData on diskQuestDB 10.1 as Parquet zstd 9, data on disk: 1.4× (233 vs 163.3 MiB; MorletDB better)QuestDB 10.1 vs MorletDB 0.3.7-alpha.1, B3.15, data on disk: 7.1× (1,163 vs 163.3 MiB; MorletDB better)CrateDB 6.4, data on disk: 11.1× (1,786 vs 161.5 MiB; MorletDB better)IoTDB 2.0, data on disk: 4.1× (660 vs 161.5 MiB; MorletDB better)TDengine 3.4, data on disk: 3.5× (567 vs 161.5 MiB; MorletDB better)TimescaleDB 2.31, data on disk: 5.0× (815 vs 161.5 MiB; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.9-alpha.1, B3.18, data on disk: 25.4× (4,156 vs 163.3 MiB; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, B3.18, data on disk: 2.5× (407 vs 163.3 MiB; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, B3.21, data on disk: 1.2× (199 vs 163.4 MiB; MorletDB better)Ingestion, one connectionQuestDB 10.1 vs MorletDB 0.3.7-alpha.1, compressed transfer, B3.16, ingestion, one connection: 1.6× (MorletDB 13.8 vs 8.9 M samples/s; MorletDB better)CrateDB 6.4, ingestion, one connection: 17.1× (MorletDB 11.6 vs 0.68 M samples/s; MorletDB better)IoTDB 2.0, ingestion, one connection: 5.2× (MorletDB 13.92 vs 2.69 M samples/s; MorletDB better)TDengine 3.4, ingestion, one connection: 3.0× (MorletDB 14.28 vs 4.79 M samples/s; MorletDB better)TimescaleDB 2.31, ingestion, one connection: 15.4× (MorletDB 13.84 vs 0.9 M samples/s; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.9-alpha.1, B3.18, ingestion, one connection: 5.0× (MorletDB 13.39 vs 2.66 M samples/s; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, B3.18, ingestion, one connection: 15.0× (MorletDB 13.92 vs 0.93 M samples/s; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, B3.21, ingestion, one connection: 18.7× (MorletDB 14.59 vs 0.78 M samples/s; MorletDB better)Read 4,096 samplesQuestDB 10.1 vs MorletDB 0.3.7-alpha.1, compressed transfer, B3.16, read 4,096 samples: 1.4× (0.86 vs MorletDB 0.63 ms; MorletDB better)CrateDB 6.4, read 4,096 samples: 11.0× (7.16 vs MorletDB 0.65 ms; MorletDB better)IoTDB 2.0, read 4,096 samples: 11.7× (8.51 vs MorletDB 0.73 ms; MorletDB better)TDengine 3.4, read 4,096 samples: 5.9× (3.65 vs MorletDB 0.62 ms; MorletDB better)TimescaleDB 2.31, read 4,096 samples: 2.4× (1.55 vs MorletDB 0.65 ms; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.9-alpha.1, B3.18, read 4,096 samples: 4.4× (2.7 vs MorletDB 0.62 ms; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, B3.18, read 4,096 samples: 15.1× (9.36 vs MorletDB 0.62 ms; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, B3.21, read 4,096 samples: 3.4× (2.22 vs MorletDB 0.65 ms; MorletDB better)Read 128,000 samplesQuestDB 10.1 vs MorletDB 0.3.7-alpha.1, compressed transfer, B3.16, read 128,000 samples: 3.1× (8.28 vs MorletDB 2.65 ms; MorletDB better)CrateDB 6.4, read 128,000 samples: 6.8× (54.33 vs MorletDB 7.97 ms; MorletDB better)IoTDB 2.0, read 128,000 samples: 4.8× (42.44 vs MorletDB 8.91 ms; MorletDB better)TDengine 3.4, read 128,000 samples: 7.3× (48.75 vs MorletDB 6.72 ms; MorletDB better)TimescaleDB 2.31, read 128,000 samples: 5.0× (40.54 vs MorletDB 8.16 ms; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.9-alpha.1, B3.18, read 128,000 samples: 6.2× (35.78 vs MorletDB 5.75 ms; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, B3.18, read 128,000 samples: 4.3× (25.56 vs MorletDB 5.89 ms; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, B3.21, read 128,000 samples: 2.3× (14.23 vs MorletDB 6.19 ms; MorletDB better)Interval statisticsQuestDB 10.1 vs MorletDB 0.3.7-alpha.1, compressed transfer, B3.16, interval statistics: 5.4× (2.88 vs MorletDB 0.53 ms; MorletDB better)CrateDB 6.4, interval statistics: 1.7× (5.06 vs MorletDB 3.04 ms; MorletDB better)IoTDB 2.0, interval statistics: 2.8× (7.49 vs MorletDB 2.63 ms; MorletDB better)TDengine 3.4, interval statistics: 1.5× (3.74 vs MorletDB 2.52 ms; MorletDB better)TimescaleDB 2.31, interval statistics: 3.9× (10.16 vs MorletDB 2.62 ms; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.10-alpha.1, B3.19, mean of its 2 valid rounds; no statement (under 10%), interval statistics: 1.1× (0.519 vs MorletDB 0.481 ms; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, B3.18, interval statistics: 25.8× (17.53 vs MorletDB 0.68 ms; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, B3.21, interval statistics: 5.2× (2.312 vs MorletDB 0.445 ms; MorletDB better)Smallest memory capQuestDB 10.1 vs MorletDB 0.3.7-alpha.1, Study M1, smallest memory cap: 8.0× (completes the workload within 2 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)CrateDB 6.4 vs MorletDB 0.3.7-alpha.1, Study M1, smallest memory cap: 4.0× (completes the workload within 1 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)IoTDB 2.0 vs MorletDB 0.3.7-alpha.1, Study M1, smallest memory cap: 8.0× (completes the workload within 2 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)TDengine 3.4 vs MorletDB 0.3.7-alpha.1, Study M1, smallest memory cap: 4.0× (completes the workload within 1 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)TimescaleDB 2.31 vs MorletDB 0.3.7-alpha.1, Study M1, smallest memory cap: 8.0× (completes the workload within 2 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)CedarDB 2026-11 vs MorletDB 0.3.9-alpha.1, Study M1b, smallest memory cap: 4.0× (completes the workload within 1 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)InfluxDB 3.12 vs MorletDB 0.3.9-alpha.1, Study M1c, smallest memory cap: 16.0× (completes the workload within 4 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)GreptimeDB 1.3 vs MorletDB 0.3.12-alpha.1, Study M1d, smallest memory cap: 4.0× (completes the workload within 1 GB vs MorletDB 256 MB; caps are powers of two; MorletDB better)
How these ratios were measuredEvery 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.

Machine Apple M5 Max, 8 CPUs · 16 GB per databaseVersions MorletDB 0.3.7-alpha.1 (SQL 0.3.5), compressed transfer from the Python SDK · QuestDB 10.1.0Reads warm cache, 50 repetitions × 20 intervals, p50 / p95Ingestion median of three fresh loads, alternating with MorletDB in one session; parallel writers (B3.17, MorletDB 0.3.9-alpha.1): same number of processes for both systems
MeasureMorletDBQuestDB 10.1Result
Data on disk163 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 declared151 MiB233 MiB (Parquet zstd 9)35% smaller; the value grid is detected automatically (G1)
Short exact read, 4,096 samples0.63 / 1.03 ms0.86 / 1.66 ms1.4× faster at p50, 1.6× at p95
Three channels × 64,000 samples3.5 / 5.0 ms10.5 / 12.4 ms3.0× faster at p50, 2.5× at p95
Short exact read of recent data, 4,096 samples0.69 / 1.31 ms0.69 / 1.47 msNo statement: on par at p50, MorletDB 12% faster at p95
Short exact read of older data, 4,096 samples0.61 / 0.93 ms0.90 / 1.79 ms1.5× faster at p50, 1.9× at p95
Interval statistics (min, max, mean, RMS)0.53 / 0.59 ms2.88 / 5.84 ms5.4× faster at p50, 9.8× at p95, from chunk summaries (on by default)
Concurrent short reads, 4 clients3,974 queries/s1,978 queries/s2.0× the throughput; p95 latency 1.7 vs 5.3 ms
Long exact read, 128,000 samples2.7 / 3.8 ms8.3 / 10.8 ms3.1× faster at p50, 2.8× at p95
Ingestion, 8 parallel writers per system (B3.17)22.4 M samples/s14.0 M samples/s1.6× faster, using 2.1 of QuestDB’s 7.2 cores (durable commits: 23.2 vs 12.8 M, 1.8×)
Ingestion, one client connection13.8 M samples/s8.9 M samples/s1.55× faster (durable commits: 13.4 vs 8.1 M, 1.65×)
Smallest memory cap that completes the workload (Study M1)256 MB2 GB8× less (caps are powers of two, so coarse)
Server memory while ingesting, 16 GB container: the footprint with the documented configuration, not a need177 MiB2.2 GiB13× less
Full workflow within 2 CPUs and 512 MBpasses; same speed as unlimited; no out-of-memory eventnot measuredMorletDB alone, container limits (X1)
Altered retry of stored datarefusedsilently replaces the stored valueStored 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 ↗.

Benchmarks / search

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.

Machine Apple M5 Max, PostgreSQL 17, 8 parallel workersQueries 100 per gallery, two rounds, p95Data 4,096-sample windows, CX seismic network
GalleryExact, screened (0.3.3)Exact, every windowResult
234,762 windows138 ms516 ms3.7× faster; median 77 ms (probe search: 64 ms)
504,650 windows221 ms808 ms3.7× faster
1,026,010 windows540 ms1,928 ms3.6× faster
Probe search (indexed vector search, pgvector)88 / 130 / 237 msrecall@10 0.990–0.998Probes 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 ↗.

At the edge

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.

512 MBmemory limit for the full workflow (X1)
~16 MiBserver process memory; the rest is cache
±1%ingestion speed within 2 CPUs and 512 MB vs. no limits
Line illustration of a single-board computer: a 4-core chip, a 40-pin header, USB and Ethernet ports, and a microSD slot 4× A531.4 GHz 1 GB RAM USBUSB ETH SD Wi-Fi 40-PIN HEADER
TESTED ON A RASPBERRY PI 3B+ · X2

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 ↗).

SELF-MAINTAINING · STEWARD · 0.3.8-alpha.1

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 DATA

Not 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:

1/15of the lossless size
7 of 7bearing faults detected no later than on the original
3.1×real time at 64 kHz, one core of a Pi 3B+
Results by dataset, and where it falls short
At 0.1 bytes per sampleWaveletSZ3Downsampling
Error, bearings, XJTU-SY (lower is better)0.700.970.76
Error, bearings, Paderborn0.550.720.63
Error, CNC milling0.390.760.49
Error, seismic0.0560.0930.080
Error, screwdriving cycles0.078cannot reach0.070
Bearing run to failure: faults detected late (of 7)05 (one never)2
CNC milling: failed runs separable, AUROC (original 0.682)0.6710.7400.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)
DimensionWorksLimit or knee
Sample rate1/60 Hz to 64 kHz, all exact and equally fastabove 200 kHz refused; time-to-sample conversion off by one at rates such as 1/60 Hz (fix pending)
One recordingup to 109 samples: 7.2 M samples/s in, 24 M out, 4,096-sample reads 0.6–0.7 ms anywherestatistics over 106 samples 8.9× slower than on short recordings; below ~105 samples per recording fixed overhead dominates
Valuescounts, decimals, ADC values, floats, constants, denormals, ±1e9, all bit-exactNaN, ±Inf and values beyond ±1e9 refused
Ingest16 writers: 21 M samples/s; gaps, altered resends and overlaps refused, identical replays acceptedchunks under ~4,096 samples cost speed and space (64: 5.5× slower)
Memory, CPU108 samples within 256 MB, no out-of-memory kill; 1 CPU: ingest −28%read-back −31% at 256 MB
Full diskthe failing write is reported to the clientPostgreSQL 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 ↗

One file

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.

293 / 293chunks byte-identical to the server's for the same samples
254lines: a second reader, standard library only
8corpus cases covering every chunk kind, checked in CI
Engineering you can check

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.

Protocols frozen before measuring

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 sample read back

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.

The codec is machine-checked

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).

Upgrades and releases are tried

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.

PostgreSQL underneath

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.

Agents stay read-only

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.

Scrubbed on demand

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.

Research

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.

97.7%Mean recall@10
57 msProbe search · p95
740 msFastest in-database exact path · p95, at the time of the report

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
Direction

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 DOWN
Slices, not one big tableStorage and indexes should be cut by time, so that a search, an index rebuild or a retention step touches only the period it concerns, and a time and metadata filter decides which slices a search visits at all.
Exact summaries at every scaleShipped: per-chunk summaries and exact roll-ups over blocks of chunks keep statistics over a year in milliseconds, bit for bit. Next: they should be what remains when raw samples move on.
Tiers that keep their proofOld recordings belong on cheaper storage as checksummed files, with their metadata, context and summaries still in the database, so they can be found, judged and restored without guesswork.
EPISODIC MEMORY: REMEMBER LIKE AN ENGINEER
Episodes, with their outcomeAn incident, an operation or a confirmed investigation is an episode: an exact interval, its operating context and what it turned out to be. Shipped: episodes with context and importance. Next: recall that returns all three, so the next investigation starts where the last one ended.
Similarity, recency, importanceCloseness of shape alone is not memory. Recall should weigh how similar, how recent and how important an episode is, and a library of known incidents should stay searchable however old it is.
Forgetting on purposeA memory that only accumulates drowns in repetition. Shipped: four graded levels (not searchable, represented by another episode, summarized, gone), each move in one transaction with a ledger row; study F1 measured what forgetting costs recall and recommends recency as the default. Next: policies that propose the moves, approved by a person.

The argument is set out in report 2, Machines with a Memory (draft) ↗; F1 results in forgetting_f1 ↗.

Status

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.