NanoTS

High-Performance, Embedded Time-Series Database
Optimized for real-time streaming applications like video, finance, and IoT sensor data

Blazing Fast Performance

8.83ΞΌs
Write time on SSD
113K+
Writes/second sustained
<1ΞΌs
Read time on SSD
Lock-free
Storage data structure

Use Cases

πŸ“Ή

Video Surveillance

Built for this from day one. Capture dozens of camera streams concurrently while reviewers scrub through history with frame-accurate seeking β€” no keyframe snapping, no playback lag.

πŸš—

Self-Driving Cars

Ultra-fast writes handle multiple camera and sensor streams in real time. Microsecond timestamps and lock-free reads make on-vehicle log replay practical for debugging and ML pipeline development.

πŸ“Š

IoT Sensor Data

High-frequency sensor readings, efficient storage of numeric telemetry, and time-based queries and analysis.

πŸ’°

Financial Data

Trade tick data with microsecond precision, market data replay systems, and low-latency historical queries.

Scientific Applications

Anywhere you have multiple synchronized data streams, monotonic timestamps, and need to seek to an event and replay a window around it, NanoTS is in its element. The same primitives that power surveillance scrubbing are useful across the sciences.

πŸ”­

Radio Astronomy & Interferometry

Multi-antenna acquisition with nanosecond timestamp alignment. Capture continuous I/Q samples at line rate and seek directly to events of interest without paying for full-file scans.

🧠

Electrophysiology & Neuroscience

Hundreds-to-thousands of electrode channels at kHz–MHz rates. Stream-per-channel storage with fast windowed retrieval around behavioral or stimulus events β€” the foundational access pattern for spike-sorting and trial analysis.

🌍

Seismology & Geophysics

Continuous 24/7 acquisition from station networks. When an event occurs, pull the same time window from every nearby station instantly β€” no miniSEED file juggling, no per-query parsing overhead.

🦾

Robotics & SLAM

Lidar, IMU, GPS, and camera streams captured at native rates with precise time alignment. Frame-accurate scrubbing across heterogeneous sensors makes log replay and dataset curation a first-class workflow.

Key Features

⚑

Ultra-Fast Writes

Memory-mapped storage with lock-free data structures enables maximum throughput with sub-microsecond performance.

🧡

Lock-Free Reads

Memory-mapped data structures let any number of readers iterate, seek, and scrub concurrently while a writer is appending β€” no contention, no locks, no surprises.

πŸ”§

Configurable Durability

Trade-off between performance and data safety with configurable block sizes to match your application needs.

πŸ”„

Crash Recovery

Automatic detection and recovery from unexpected shutdowns with frame-level atomicity guarantees.

πŸ”€

Multiple Streams

Store different data streams in the same database file with independent write contexts and iterators.

πŸ”“

Open Source

Free for commercial and non-commercial use under the Apache 2.0 license.

Quick Start

Building

git clone https://github.com/dicroce/nanots.git

Copy the 4 source files from the nanots/amalgamated_src/ dir into your projects source directories and add them to your build.

Or use CMake to build as a static library and link it (mkdir build && cd build && cmake .. && make)
                    
Writing Data
#include "nanots.h"

// Create a growable database with 1MB blocks. The file starts at just
// the 64KB header and extends on demand as you write.
nanots_writer::allocate_growable("video.nts", 1024*1024);

nanots_writer db("video.nts");

// Create write context for a stream
auto wctx = db.create_write_context("camera_1", "stream metadata");

// Write frames
uint8_t frame_data[] = {/* video frame bytes */};
db.write(wctx, frame_data, sizeof(frame_data), timestamp_us, flags);
Reading Data
#include "nanots.h"

// Create iterator for a stream
nanots_iterator iter("video.nts", "camera_1");

// Iterate through all frames
while (iter.valid()) {
    auto& frame = *iter;
    process_frame(frame.data, frame.size, frame.timestamp, frame.flags);
    ++iter;
}

// Or seek to specific timestamp
if (iter.find(target_timestamp)) {
    // Found first frame >= target_timestamp
    auto& frame = *iter;
    // ... process frame
}

Architecture

Storage Layout

    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚File Header  β”‚   Block 1   β”‚   Block 2   β”‚   Block N   β”‚
    β”‚   (64KB)    β”‚   (1MB+)    β”‚   (1MB+)    β”‚   (1MB+)    β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                /                  \
                /                     \
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚Block Header β”‚   Block Index   β”‚        Block Data        β”‚
    β”‚ (16 bytes)  β”‚   (variable) -> β”‚      <- (variable)       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    

Hybrid approach combining SQLite for metadata with memory-mapped binary files for data. Block size is configurable and tunable for different applications.

Frequently Asked Questions

How durable is NanoTS?

Durability is configurable through block sizes. Writers sync to disk during block transitions. Smaller blocks provide better durability but may slow writes due to contention. We recommend starting with large blocks (50MB) and adjusting as needed.

How many writers and readers are supported?

NanoTS allows any number of writing threads as long as each writes to its own logical stream, plus any number of read threads (1 writer per stream + N readers).

How does storage allocation work?

By default, NanoTS files grow on demand β€” the file starts as just a 64KB header and extends as you write, using a BoltDB-style doubling strategy capped at 1 GiB per grow event. For embedded or fixed-budget deployments you can also allocate a file at a fixed size up front and combine it with auto-recycle mode so the oldest data is reclaimed when the file is full.