GitHub

ArcherDB Documentation

ArcherDB is a distributed geospatial database built for real-time location tracking at scale. It combines the consistency guarantees of Viewstamped Replication (VSR) with a high-performance S2-based spatial index, enabling sub-millisecond queries across billions of location events.

Security and continuity controls are infrastructure-managed by default: authn/authz, TLS/mTLS, encryption at rest, and backup orchestration are expected to be enforced outside of ArcherDB itself.

Guide Time Description
Quickstart 5 min Hello world - first query
Getting Started 10 min Comprehensive setup and usage
API Reference - Complete operation documentation

For Developers

Tutorials

Learn ArcherDB step-by-step:

  • Quickstart - Insert and query your first location in 5 minutes
  • Getting Started - Comprehensive setup, SDK installation, and usage patterns

How-To Guides

Goal-oriented guides:

Reference

Complete API and configuration documentation:

SDK Documentation

Comprehensive guides for each language:

Language Package Documentation
Python archerdb src/clients/python/README.md
Node.js archerdb-node src/clients/node/README.md
Go archerdb-go src/clients/go/README.md
Java com.archerdb:archerdb-java (local install until Central publish) src/clients/java/README.md
C libarcherdb src/clients/c/README.md

For Operators

Deployment

Recovery & Continuity

Troubleshooting

Alert Runbooks

Per-alert response guides linked from Prometheus alerts:

Performance

Testing & CI

Understanding ArcherDB

Architecture and design:

Security

Internals

For contributors:

Release Notes

  • CHANGELOG.md - Release history and notable changes
  • FINALIZATION_PLAN.md - Release hardening and exit criteria

Documentation Coverage

All documentation requirements (DOCS-01 through DOCS-08) are complete:

Requirement Documentation
DOCS-01: Getting started quickstart.md, getting-started.md
DOCS-02: API reference api-reference.md, openapi.yaml
DOCS-03: Operations runbook operations-runbook.md, runbooks/
DOCS-04: Troubleshooting troubleshooting.md
DOCS-05: Architecture architecture.md
DOCS-06: Performance tuning lsm-tuning.md, profiling.md, BENCHMARKS.md
DOCS-07: Security security-best-practices.md, encryption-guide.md
DOCS-08: SDK documentation sdk/, src/clients/*/README.md

See .planning/REQUIREMENTS.md for full traceability.

Edit this page

Quickstart

Time to complete: ~5 minutes

Get from zero to your first spatial query in 5 minutes. For comprehensive setup, see the Getting Started Guide.

Step 1: Download Binary (~1 min)

Linux (x86_64)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-linux-x86_64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/
macOS (Apple Silicon)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-macos-aarch64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/
macOS (Intel)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-macos-x86_64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/

Step 2: Start Server (~1 min)

# Format data file and start server
archerdb format --cluster=0 --replica=0 --replica-count=1 data.archerdb
archerdb start --addresses=3000 data.archerdb

You should see: info: server ready on 127.0.0.1:3000

Open a new terminal for the next steps.

Step 3: Install SDK (~1 min)

Python
pip install archerdb
Node.js
npm install archerdb-node
Go
go get github.com/ArcherDB-io/archerdb/src/clients/go
Java (source checkout / local Maven install)

Until archerdb-java is explicitly published to Maven Central, use a source checkout:

./zig/zig build clients:java -Drelease
(cd src/clients/java && mvn --batch-mode --quiet install)

Then use the local Maven artifact version from src/clients/java/pom.xml, which is currently 0.1.0-SNAPSHOT.

curl (no SDK needed)

No installation required - use curl directly.

Step 4: Insert a Location (~1 min)

Insert a delivery vehicle location in San Francisco:

Python
import archerdb

client = archerdb.GeoClientSync(archerdb.GeoClientConfig(
    cluster_id=0,
    addresses=['127.0.0.1:3000']
))

# Insert vehicle at SF city center (37.7749, -122.4194)
event = archerdb.create_geo_event(
    entity_id=archerdb.id(),
    latitude=37.7749,
    longitude=-122.4194,
    group_id=1,
)
client.insert_events([event])
print(f"Inserted vehicle: {event.entity_id}")
Node.js
const { createGeoClient, createGeoEvent, id } = require('archerdb-node')

const client = createGeoClient({
  cluster_id: 0n,
  addresses: ['127.0.0.1:3000'],
})

// Insert vehicle at SF city center (37.7749, -122.4194)
const event = createGeoEvent({
  entity_id: id(),
  latitude: 37.7749,
  longitude: -122.4194,
  group_id: 1n,
})

await client.insertEvents([event])
console.log(`Inserted vehicle: ${event.entity_id}`)
client.destroy()
Go
package main

import (
    "fmt"
    "log"
    archerdb "github.com/ArcherDB-io/archerdb/src/clients/go"
    "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/types"
)

func main() {
    client, _ := archerdb.NewGeoClient(archerdb.GeoClientConfig{
        ClusterID: types.ToUint128(0),
        Addresses: []string{"127.0.0.1:3000"},
    })
    defer client.Close()

    // Insert vehicle at SF city center (37.7749, -122.4194)
    event, _ := types.NewGeoEvent(types.GeoEventOptions{
        EntityID:  types.ID(),
        Latitude:  37.7749,
        Longitude: -122.4194,
        GroupID:   1,
    })
    client.InsertEvents([]types.GeoEvent{event})
    fmt.Printf("Inserted vehicle: %s\n", event.EntityID)
}
Java
import com.archerdb.geo.*;

public class Quickstart {
    public static void main(String[] args) throws Exception {
        try (GeoClient client = GeoClient.create(0L, "127.0.0.1:3000")) {
            // Insert vehicle at SF city center (37.7749, -122.4194)
            UInt128 entityId = UInt128.random();
            GeoEvent event = new GeoEvent.Builder()
                .setEntityId(entityId)
                .setLatitude(37.7749)
                .setLongitude(-122.4194)
                .setGroupId(1L)
                .build();

            GeoEventBatch batch = client.createBatch();
            batch.add(event);
            batch.commit();
            System.out.println("Inserted vehicle: " + entityId);
        }
    }
}
curl
# Insert vehicle at SF city center (37.7749, -122.4194)
curl -X POST http://127.0.0.1:3000/insert \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "00000000-0000-0000-0000-000000000001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }]
  }'

Step 5: Query by Radius (~1 min)

Find all vehicles within 1km of SF city center:

Python
# Find vehicles within 1km of SF city center
results = client.query_radius(
    center_lat=37.7749,
    center_lon=-122.4194,
    radius_m=1000,
)
print(f"Found {len(results.events)} vehicles within 1km")
Node.js
// Find vehicles within 1km of SF city center
const results = await client.queryRadius({
  latitude: 37.7749,
  longitude: -122.4194,
  radius_m: 1000,
})
console.log(`Found ${results.events.length} vehicles within 1km`)
Go
// Find vehicles within 1km of SF city center
filter, _ := types.NewRadiusQuery(37.7749, -122.4194, 1000, 100)
results, _ := client.QueryRadius(filter)
fmt.Printf("Found %d vehicles within 1km\n", len(results.Events))
Java
// Find vehicles within 1km of SF city center
QueryResult results = client.queryRadius(
    QueryRadiusFilter.create(37.7749, -122.4194, 1000, 100)
);
System.out.println("Found " + results.getEvents().size() + " vehicles within 1km");
curl
# Find vehicles within 1km of SF city center
curl -X POST http://127.0.0.1:3000/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat_nano": 37774900000,
    "center_lon_nano": -122419400000,
    "radius_m": 1000,
    "limit": 100
  }'

Congratulations! You just completed your first spatial query.

You inserted a location and found it with a radius query - the core of real-time location tracking.

Next Steps

Edit this page

Getting Started with ArcherDB

Time to First Query

Total time: ~10 minutes

Section Time Description
Prerequisites 0 min Verify requirements
Installation 2 min Download binary
Starting cluster 1 min Format and start
SDK installation 1 min Install your language
Hello World 3 min Insert and query
Next steps 1 min What to explore

This guide gets you to your first spatial query quickly, then shows you the full capabilities.

Prerequisites

  • Operating System: Linux (kernel >= 5.6), macOS, or Windows
  • For SDKs: Python 3.9+, Node.js 18+, Go 1.21+, or Java 11+

Security Boundary Note

ArcherDB expects security controls at the infrastructure boundary:

  • Authentication/authorization at your API or gateway layer
  • TLS/mTLS in gateway/service mesh or private network transport
  • Encryption at rest and key management in storage/cloud platform
  • Backup: ArcherDB has built-in upload to S3, GCS, and Azure Blob (see Backup Operations); platform snapshots remain a defense-in-depth option

Installation (~2 min)

Download the pre-built binary for your platform:

Linux (x86_64)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-linux-x86_64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/
archerdb --version
macOS (Apple Silicon)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-macos-aarch64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/
archerdb --version
macOS (Intel)
curl -L https://github.com/ArcherDB-io/archerdb/releases/latest/download/archerdb-macos-x86_64.tar.gz | tar xz
sudo mv archerdb /usr/local/bin/
archerdb --version
Build from Source
git clone https://github.com/ArcherDB-io/archerdb.git
cd archerdb
./zig/download.sh
./zig/zig build
# Binary at ./zig-out/bin/archerdb

Choose a Tier

ArcherDB tier presets are open-source runtime and capacity profiles:

  • lite (recommended for demos/evaluation): fastest first-run experience, low footprint, intentionally storage-limited.
  • standard: baseline production profile.
  • pro: higher-performance mainstream profile.
  • enterprise: high-end production profile.
  • ultra: top-end profile.

If building from source, choose a tier explicitly:

./zig/zig build -Dconfig=lite
# or: standard, pro, enterprise, ultra

Starting a Cluster (~1 min)

Single-Node Development

# Format data file (cluster=0 for dev)
archerdb format --cluster=0 --replica=0 --replica-count=1 data.archerdb

# Start server on port 3000
archerdb start --addresses=3000 data.archerdb

You should see: info: server ready on 127.0.0.1:3000

Production Three-Node Cluster

For fault tolerance (survives 1 node failure):

# Node 1 (192.168.1.1)
archerdb format --cluster=12345 --replica=0 --replica-count=3 /data/archerdb.db
archerdb start --addresses=192.168.1.1:3000,192.168.1.2:3000,192.168.1.3:3000 /data/archerdb.db

# Node 2 (192.168.1.2)
archerdb format --cluster=12345 --replica=1 --replica-count=3 /data/archerdb.db
archerdb start --addresses=192.168.1.1:3000,192.168.1.2:3000,192.168.1.3:3000 /data/archerdb.db

# Node 3 (192.168.1.3)
archerdb format --cluster=12345 --replica=2 --replica-count=3 /data/archerdb.db
archerdb start --addresses=192.168.1.1:3000,192.168.1.2:3000,192.168.1.3:3000 /data/archerdb.db

SDK Installation (~1 min)

Python
pip install archerdb
Node.js
npm install archerdb-node
Go
go get github.com/ArcherDB-io/archerdb/src/clients/go
Java (source checkout / local Maven install)

Until archerdb-java is explicitly published to Maven Central, use a source checkout:

./zig/zig build clients:java -Drelease
(cd src/clients/java && mvn --batch-mode --quiet install)

Then depend on the local Maven artifact version from src/clients/java/pom.xml, which is currently 0.1.0-SNAPSHOT:

<dependency>
    <groupId>com.archerdb</groupId>
    <artifactId>archerdb-java</artifactId>
    <version>0.1.0-SNAPSHOT</version>
</dependency>

Or Gradle:

implementation 'com.archerdb:archerdb-java:0.1.0-SNAPSHOT'
curl (no SDK)

Use curl directly - no installation needed.

Backup Configuration (Optional)

ArcherDB can stream every closed LSM block to S3, GCS, or Azure Blob storage as the cluster runs. Append the relevant flags to archerdb start:

# S3 / S3-compatible (MinIO, R2, Backblaze, LocalStack)
archerdb start \
  --backup-enabled \
  --backup-provider=s3 \
  --backup-region=us-east-1 \
  --backup-bucket=my-archerdb-backups \
  data.archerdb

# Azure Blob (SharedKey)
archerdb start \
  --backup-enabled \
  --backup-provider=azure \
  --backup-bucket=my-archerdb-container \
  --backup-access-key-id=<account-name> \
  --backup-secret-access-key=<base64-account-key> \
  data.archerdb

S3 credentials are picked up from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY when not provided on the command line. GCS uses the same flags as S3 with --backup-provider=gcs and an HMAC key issued via the Cloud Storage Interoperability console. Restore via archerdb restore from any of the supported providers; see Backup Operations and Disaster Recovery.

Hello World: Vehicle Tracking (~3 min)

This example demonstrates the core value of ArcherDB: track vehicles and find nearby pickups.

Scenario: A delivery vehicle needs to find pickup locations within 1km.

Step 1: Create Client

Python
import archerdb

client = archerdb.GeoClientSync(archerdb.GeoClientConfig(
    cluster_id=0,
    addresses=['127.0.0.1:3000']
))
Node.js
const { createGeoClient, createGeoEvent, id } = require('archerdb-node')

const client = createGeoClient({
  cluster_id: 0n,
  addresses: ['127.0.0.1:3000'],
})
Go
import (
    archerdb "github.com/ArcherDB-io/archerdb/src/clients/go"
    "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/types"
)

client, err := archerdb.NewGeoClient(archerdb.GeoClientConfig{
    ClusterID: types.ToUint128(0),
    Addresses: []string{"127.0.0.1:3000"},
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()
Java
import com.archerdb.geo.*;

GeoClient client = GeoClient.create(0L, "127.0.0.1:3000");
curl
# No client setup needed - use curl directly
BASE_URL="http://127.0.0.1:3000"

Step 2: Insert Delivery Vehicle

Insert the vehicle at SF city center (37.7749, -122.4194):

Python
vehicle_id = archerdb.id()
vehicle = archerdb.create_geo_event(
    entity_id=vehicle_id,
    latitude=37.7749,      # SF city center
    longitude=-122.4194,
    group_id=1,            # Fleet ID
)
client.insert_events([vehicle])
print(f"Vehicle {vehicle_id} inserted")
Node.js
const vehicleId = id()
const vehicle = createGeoEvent({
  entity_id: vehicleId,
  latitude: 37.7749,      // SF city center
  longitude: -122.4194,
  group_id: 1n,           // Fleet ID
})
await client.insertEvents([vehicle])
console.log(`Vehicle ${vehicleId} inserted`)
Go
vehicleID := types.ID()
vehicle, _ := types.NewGeoEvent(types.GeoEventOptions{
    EntityID:  vehicleID,
    Latitude:  37.7749,      // SF city center
    Longitude: -122.4194,
    GroupID:   1,            // Fleet ID
})
client.InsertEvents([]types.GeoEvent{vehicle})
fmt.Printf("Vehicle %s inserted\n", vehicleID)
Java
UInt128 vehicleId = UInt128.random();
GeoEvent vehicle = new GeoEvent.Builder()
    .setEntityId(vehicleId)
    .setLatitude(37.7749)      // SF city center
    .setLongitude(-122.4194)
    .setGroupId(1L)            // Fleet ID
    .build();

GeoEventBatch batch = client.createBatch();
batch.add(vehicle);
batch.commit();
System.out.println("Vehicle " + vehicleId + " inserted");
curl
curl -X POST $BASE_URL/insert \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "00000000-0000-0000-0000-000000000001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }]
  }'

Step 3: Insert Nearby Pickup Locations

Add two pickup locations near the vehicle:

Location Coordinates Distance from Vehicle
Pickup 1 37.7751, -122.4180 ~200m east
Pickup 2 37.7760, -122.4200 ~150m north
Python
# Pickup 1: 200m east of vehicle
pickup1 = archerdb.create_geo_event(
    entity_id=archerdb.id(),
    latitude=37.7751,
    longitude=-122.4180,
    group_id=2,  # Pickups group
)

# Pickup 2: 150m north of vehicle
pickup2 = archerdb.create_geo_event(
    entity_id=archerdb.id(),
    latitude=37.7760,
    longitude=-122.4200,
    group_id=2,
)

client.insert_events([pickup1, pickup2])
print("2 pickup locations inserted")
Node.js
// Pickup 1: 200m east of vehicle
const pickup1 = createGeoEvent({
  entity_id: id(),
  latitude: 37.7751,
  longitude: -122.4180,
  group_id: 2n,  // Pickups group
})

// Pickup 2: 150m north of vehicle
const pickup2 = createGeoEvent({
  entity_id: id(),
  latitude: 37.7760,
  longitude: -122.4200,
  group_id: 2n,
})

await client.insertEvents([pickup1, pickup2])
console.log('2 pickup locations inserted')
Go
// Pickup 1: 200m east of vehicle
pickup1, _ := types.NewGeoEvent(types.GeoEventOptions{
    EntityID:  types.ID(),
    Latitude:  37.7751,
    Longitude: -122.4180,
    GroupID:   2,  // Pickups group
})

// Pickup 2: 150m north of vehicle
pickup2, _ := types.NewGeoEvent(types.GeoEventOptions{
    EntityID:  types.ID(),
    Latitude:  37.7760,
    Longitude: -122.4200,
    GroupID:   2,
})

client.InsertEvents([]types.GeoEvent{pickup1, pickup2})
fmt.Println("2 pickup locations inserted")
Java
// Pickup 1: 200m east of vehicle
GeoEvent pickup1 = new GeoEvent.Builder()
    .setEntityId(UInt128.random())
    .setLatitude(37.7751)
    .setLongitude(-122.4180)
    .setGroupId(2L)  // Pickups group
    .build();

// Pickup 2: 150m north of vehicle
GeoEvent pickup2 = new GeoEvent.Builder()
    .setEntityId(UInt128.random())
    .setLatitude(37.7760)
    .setLongitude(-122.4200)
    .setGroupId(2L)
    .build();

GeoEventBatch batch = client.createBatch();
batch.add(pickup1);
batch.add(pickup2);
batch.commit();
System.out.println("2 pickup locations inserted");
curl
curl -X POST $BASE_URL/insert \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "entity_id": "00000000-0000-0000-0000-000000000002",
        "lat_nano": 37775100000,
        "lon_nano": -122418000000,
        "group_id": 2
      },
      {
        "entity_id": "00000000-0000-0000-0000-000000000003",
        "lat_nano": 37776000000,
        "lon_nano": -122420000000,
        "group_id": 2
      }
    ]
  }'

Step 4: Find Pickups Within 1km of Vehicle

Query for pickups near the vehicle’s location:

Python
# Find all entities within 1km of vehicle
results = client.query_radius(
    center_lat=37.7749,
    center_lon=-122.4194,
    radius_m=1000,  # 1 kilometer
    limit=100,
)

print(f"Found {len(results.events)} entities within 1km of vehicle:")
for event in results.events:
    print(f"  Entity {event.entity_id} at ({event.latitude:.4f}, {event.longitude:.4f})")

Output:

Found 3 entities within 1km of vehicle:
  Entity 1234... at (37.7749, -122.4194)  # Vehicle
  Entity 5678... at (37.7751, -122.4180)  # Pickup 1
  Entity 9012... at (37.7760, -122.4200)  # Pickup 2
Node.js
// Find all entities within 1km of vehicle
const results = await client.queryRadius({
  latitude: 37.7749,
  longitude: -122.4194,
  radius_m: 1000,  // 1 kilometer
  limit: 100,
})

console.log(`Found ${results.events.length} entities within 1km of vehicle:`)
for (const event of results.events) {
  console.log(`  Entity ${event.entity_id}`)
}
Go
// Find all entities within 1km of vehicle
filter, _ := types.NewRadiusQuery(37.7749, -122.4194, 1000, 100)
results, _ := client.QueryRadius(filter)

fmt.Printf("Found %d entities within 1km of vehicle:\n", len(results.Events))
for _, event := range results.Events {
    fmt.Printf("  Entity %s at (%.4f, %.4f)\n",
        event.EntityID, event.Latitude(), event.Longitude())
}
Java
// Find all entities within 1km of vehicle
QueryResult results = client.queryRadius(
    QueryRadiusFilter.create(37.7749, -122.4194, 1000, 100)
);

System.out.println("Found " + results.getEvents().size() + " entities within 1km of vehicle:");
for (GeoEvent event : results.getEvents()) {
    System.out.println("  Entity " + event.getEntityId());
}
curl
curl -X POST $BASE_URL/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat_nano": 37774900000,
    "center_lon_nano": -122419400000,
    "radius_m": 1000,
    "limit": 100
  }'

Congratulations! You’ve completed the core ArcherDB workflow: insert locations and find nearby entities.

Additional Operations

Polygon Queries (Geofencing)

Find entities within a geographic boundary:

Python
# Define a polygon (counter-clockwise winding)
polygon = [
    (37.78, -122.42),  # NW corner
    (37.78, -122.40),  # NE corner
    (37.76, -122.40),  # SE corner
    (37.76, -122.42),  # SW corner
]

results = client.query_polygon(vertices=polygon, limit=1000)
print(f"Found {len(results.events)} entities in polygon")
Node.js
const polygon = [
  { lat: 37.78, lon: -122.42 },
  { lat: 37.78, lon: -122.40 },
  { lat: 37.76, lon: -122.40 },
  { lat: 37.76, lon: -122.42 },
]

const results = await client.queryPolygon({ vertices: polygon, limit: 1000 })
console.log(`Found ${results.events.length} entities in polygon`)
Go
vertices := [][]float64{
    {37.78, -122.42},
    {37.78, -122.40},
    {37.76, -122.40},
    {37.76, -122.42},
}

filter, _ := types.NewPolygonQuery(vertices, 1000)
results, _ := client.QueryPolygon(filter)
fmt.Printf("Found %d entities in polygon\n", len(results.Events))
Java
QueryPolygonFilter filter = new QueryPolygonFilter.Builder()
    .addVertex(37.78, -122.42)
    .addVertex(37.78, -122.40)
    .addVertex(37.76, -122.40)
    .addVertex(37.76, -122.42)
    .setLimit(1000)
    .build();

QueryResult results = client.queryPolygon(filter);
System.out.println("Found " + results.getEvents().size() + " entities in polygon");
curl
curl -X POST $BASE_URL/query/polygon \
  -H "Content-Type: application/json" \
  -d '{
    "vertices": [
      {"lat_nano": 37780000000, "lon_nano": -122420000000},
      {"lat_nano": 37780000000, "lon_nano": -122400000000},
      {"lat_nano": 37760000000, "lon_nano": -122400000000},
      {"lat_nano": 37760000000, "lon_nano": -122420000000}
    ],
    "limit": 1000
  }'

Polygon with Holes

Exclude regions (parks, lakes, restricted zones):

Python
# Outer boundary (counter-clockwise)
boundary = [
    (37.79, -122.42),
    (37.79, -122.39),
    (37.76, -122.39),
    (37.76, -122.42),
]

# Hole to exclude (clockwise winding)
park_hole = [
    (37.78, -122.41),
    (37.775, -122.41),
    (37.775, -122.40),
    (37.78, -122.40),
]

results = client.query_polygon(
    vertices=boundary,
    holes=[park_hole],  # Up to 100 holes
    limit=1000,
)
Node.js
const boundary = [
  { lat: 37.79, lon: -122.42 },
  { lat: 37.79, lon: -122.39 },
  { lat: 37.76, lon: -122.39 },
  { lat: 37.76, lon: -122.42 },
]

const parkHole = [
  { lat: 37.78, lon: -122.41 },
  { lat: 37.775, lon: -122.41 },
  { lat: 37.775, lon: -122.40 },
  { lat: 37.78, lon: -122.40 },
]

const results = await client.queryPolygon({
  vertices: boundary,
  holes: [parkHole],
  limit: 1000,
})
Go
boundary := [][]float64{
    {37.79, -122.42},
    {37.79, -122.39},
    {37.76, -122.39},
    {37.76, -122.42},
}

parkHole := [][]float64{
    {37.78, -122.41},
    {37.775, -122.41},
    {37.775, -122.40},
    {37.78, -122.40},
}

filter, _ := types.NewPolygonQuery(boundary, 1000, parkHole)
results, _ := client.QueryPolygon(filter)
Java
QueryPolygonFilter filter = new QueryPolygonFilter.Builder()
    .addVertex(37.79, -122.42)
    .addVertex(37.79, -122.39)
    .addVertex(37.76, -122.39)
    .addVertex(37.76, -122.42)
    .startHole()
    .addHoleVertex(37.78, -122.41)
    .addHoleVertex(37.775, -122.41)
    .addHoleVertex(37.775, -122.40)
    .addHoleVertex(37.78, -122.40)
    .finishHole()
    .setLimit(1000)
    .build();
curl
curl -X POST $BASE_URL/query/polygon \
  -H "Content-Type: application/json" \
  -d '{
    "vertices": [
      {"lat_nano": 37790000000, "lon_nano": -122420000000},
      {"lat_nano": 37790000000, "lon_nano": -122390000000},
      {"lat_nano": 37760000000, "lon_nano": -122390000000},
      {"lat_nano": 37760000000, "lon_nano": -122420000000}
    ],
    "holes": [[
      {"lat_nano": 37780000000, "lon_nano": -122410000000},
      {"lat_nano": 37775000000, "lon_nano": -122410000000},
      {"lat_nano": 37775000000, "lon_nano": -122400000000},
      {"lat_nano": 37780000000, "lon_nano": -122400000000}
    ]],
    "limit": 1000
  }'

Polygon constraints:

  • Outer boundary: counter-clockwise winding
  • Holes: clockwise winding
  • Maximum 100 holes per polygon
  • Holes must be fully contained within boundary

Get Latest Position

Python
event = client.get_latest(entity_id)
if event:
    print(f"Last seen at: ({event.latitude:.4f}, {event.longitude:.4f})")

# Batch lookup
events = client.get_latest_batch([entity_id1, entity_id2, entity_id3])
Node.js
const event = await client.getLatest(entityId)
if (event) {
  console.log(`Last seen at: (${event.latitude}, ${event.longitude})`)
}

// Batch lookup
const events = await client.getLatestBatch([entityId1, entityId2, entityId3])
Go
event, err := client.GetLatest(entityID)
if err == nil && event != nil {
    fmt.Printf("Last seen at: (%.4f, %.4f)\n", event.Latitude(), event.Longitude())
}

// Batch lookup
events, _ := client.GetLatestBatch([]types.UInt128{entityID1, entityID2})
Java
Optional<GeoEvent> event = client.getLatest(entityId);
event.ifPresent(e ->
    System.out.println("Last seen at: (" + e.getLatitude() + ", " + e.getLongitude() + ")")
);

// Batch lookup
List<GeoEvent> events = client.getLatestBatch(List.of(entityId1, entityId2));
curl
curl -X POST $BASE_URL/query/latest \
  -H "Content-Type: application/json" \
  -d '{"entity_ids": ["00000000-0000-0000-0000-000000000001"]}'

Delete Entities (GDPR Compliance)

Python
result = client.delete_entities([entity_id1, entity_id2])
print(f"Deleted: {result.deleted_count}")
Node.js
const result = await client.deleteEntities([entityId1, entityId2])
console.log(`Deleted: ${result.deleted_count}`)
Go
result, _ := client.DeleteEntities([]types.UInt128{entityID1, entityID2})
fmt.Printf("Deleted: %d\n", result.DeletedCount)
Java
DeleteResult result = client.deleteEntities(List.of(entityId1, entityId2));
System.out.println("Deleted: " + result.getDeletedCount());
curl
curl -X POST $BASE_URL/delete \
  -H "Content-Type: application/json" \
  -d '{"entity_ids": ["00000000-0000-0000-0000-000000000001"]}'

Error Handling

All SDKs provide typed errors:

Python
from archerdb import (
    ArcherDBError,
    InvalidCoordinates,
    ClusterUnavailable,
    OperationTimeout,
)

try:
    client.insert_events(events)
except InvalidCoordinates as e:
    print(f"Bad coordinates: {e}")
except ClusterUnavailable:
    print("Cluster down, retry later")
except OperationTimeout:
    print("Timeout - operation may have committed")
Node.js
import {
  InvalidCoordinates,
  ClusterUnavailable,
  OperationTimeout,
} from 'archerdb-node'

try {
  await client.insertEvents(events)
} catch (error) {
  if (error instanceof InvalidCoordinates) {
    console.error('Bad coordinates:', error.message)
  } else if (error instanceof ClusterUnavailable) {
    console.error('Cluster down, retry later')
  } else if (error instanceof OperationTimeout) {
    console.error('Timeout - operation may have committed')
  }
}
Go
import "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/errors"

_, err := client.InsertEvents(events)
if err != nil {
    switch {
    case errors.IsInvalidCoordinates(err):
        log.Printf("Bad coordinates: %v", err)
    case errors.IsClusterUnavailable(err):
        log.Println("Cluster down, retry later")
    case errors.IsOperationTimeout(err):
        log.Println("Timeout - operation may have committed")
    }
}
Java
import com.archerdb.geo.exceptions.*;

try {
    client.insertEvents(events);
} catch (InvalidCoordinatesException e) {
    System.err.println("Bad coordinates: " + e.getMessage());
} catch (ClusterUnavailableException e) {
    System.err.println("Cluster down, retry later");
} catch (OperationTimeoutException e) {
    System.err.println("Timeout - operation may have committed");
}
curl
# HTTP status codes:
# 200 - Success
# 400 - Invalid request (bad coordinates, malformed JSON)
# 503 - Cluster unavailable
# 504 - Request timeout

Configuration Reference

Client Options

Option Default Description
cluster_id Required Cluster identifier
addresses Required List of replica addresses
connect_timeout_ms 5000 Connection timeout
request_timeout_ms 30000 Request timeout

Retry Options

Option Default Description
enabled true Enable automatic retry
max_retries 5 Maximum retry attempts
base_backoff_ms 100 Base delay for exponential backoff
max_backoff_ms 1600 Maximum backoff delay

See SDK Retry Semantics for detailed retry behavior.

What’s Next (~1 min)

Goal Resource
Complete API documentation API Reference
Deploy to production Operations Runbook
Understand architecture Architecture
Handle failures Troubleshooting
Set up backups Backup Operations
Plan for disasters Disaster Recovery

SDK Documentation

Edit this page

SDK Retry Semantics

This document describes the retry behavior of ArcherDB client SDKs, including multi-batch retry patterns, idempotency guarantees, and configuration options.

Overview

ArcherDB SDKs implement automatic retry with exponential backoff for transient failures. Retries are enabled by default and preserve idempotency guarantees, making it safe to retry operations without risk of duplicate execution.

Retry Configuration

All SDKs support the following configuration parameters:

Parameter Default Description
enabled true Enable/disable automatic retry
max_retries 5 Maximum retry attempts after initial failure
base_backoff_ms 100 Base delay for exponential backoff
max_backoff_ms 1600 Maximum backoff delay cap
total_timeout_ms 30000 Total timeout across all attempts
jitter true Add random jitter to prevent thundering herd

Node.js

import { createGeoClient, RetryConfig } from 'archerdb-node'

const client = createGeoClient({
  cluster_id: 0n,
  addresses: ['127.0.0.1:3000'],
  retry: {
    enabled: true,
    max_retries: 5,
    base_backoff_ms: 100,
    max_backoff_ms: 1600,
    total_timeout_ms: 30000,
    jitter: true,
  }
})

Python

import archerdb

client = archerdb.GeoClientSync(archerdb.GeoClientConfig(
    cluster_id=0,
    addresses=['127.0.0.1:3000'],
    retry=archerdb.RetryConfig(
        enabled=True,
        max_retries=5,
        base_backoff_ms=100,
        max_backoff_ms=1600,
        total_timeout_ms=30000,
        jitter=True,
    )
))

Go

import (
    "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/retry"
)

config := retry.DefaultConfig()
config.MaxRetries = 5
config.BaseBackoffMs = 100
config.MaxBackoffMs = 1600
config.TotalTimeoutMs = 30000
config.Jitter = true

Backoff Schedule

The SDK uses exponential backoff with the following schedule:

Attempt Base Delay With Jitter (typical)
1 0ms 0ms (immediate)
2 100ms 100-150ms
3 200ms 200-300ms
4 400ms 400-600ms
5 800ms 800-1200ms
6 1600ms 1600-2400ms

Jitter Formula: actual_delay = base_delay + random(0, base_delay / 2)

Jitter prevents the “thundering herd” problem where many clients retry simultaneously after a transient failure.

Error Classification

Retryable Errors (Transient)

These errors are automatically retried:

  • timeout - Operation timed out
  • view_change_in_progress - Leader election in progress
  • not_primary - Connected to non-primary replica
  • cluster_unavailable - No quorum available
  • session_expired - Session needs re-registration
  • Network errors (connection reset, refused, timeout)

Non-Retryable Errors (Permanent)

These errors fail immediately without retry:

  • invalid_coordinates - Coordinates out of valid range
  • polygon_too_complex - Too many vertices
  • batch_too_large - Batch exceeds maximum size
  • query_result_too_large - Query limit exceeded
  • invalid_entity_id - Zero or malformed entity ID
  • Secure-transport boundary errors (gateway/proxy certificate failures)

Multi-Batch Retry Pattern

When a large batch operation times out, the SDK cannot determine which events succeeded vs. failed. The split_batch helper enables safe retry by dividing the batch into smaller chunks.

The Problem

[Event 0] [Event 1] [Event 2] ... [Event 9999]
                         ^
                         | Timeout occurs here
                         | Events 0-2000 committed
                         | Events 2001-9999 unknown

After a timeout, you don’t know which events committed. Simply retrying the entire batch risks duplicate processing (though server-side idempotency may prevent this for some operations).

The Solution: split_batch

Split the batch into smaller chunks and retry each chunk individually:

Original:  [0..9999] → TIMEOUT

Split:     [0..999] → SUCCESS (or already committed)
           [1000..1999] → SUCCESS
           [2000..2999] → SUCCESS
           ...

Node.js

import { splitBatch, OperationTimeout } from 'archerdb-node'

const events = generateLargeEventList()

try {
  const batch = client.createBatch()
  for (const event of events) {
    batch.add(event)
  }
  await batch.commit()
} catch (error) {
  if (error instanceof OperationTimeout) {
    // Split into smaller chunks and retry
    const chunks = splitBatch(events, 500)

    for (const chunk of chunks) {
      const retryBatch = client.createBatch()
      for (const event of chunk) {
        retryBatch.add(event)
      }
      try {
        await retryBatch.commit()
      } catch (retryError) {
        if (retryError instanceof OperationTimeout) {
          // Retry with even smaller chunks
          const smallerChunks = splitBatch(chunk, 100)
          // ... continue recursively
        }
      }
    }
  }
}

Python

from archerdb import split_batch, OperationTimeout

events = generate_large_event_list()

try:
    batch = client.create_batch()
    for event in events:
        batch.add(event)
    batch.commit()
except OperationTimeout:
    # Split into smaller chunks and retry
    chunks = split_batch(events, 500)

    for chunk in chunks:
        retry_batch = client.create_batch()
        for event in chunk:
            retry_batch.add(event)
        try:
            retry_batch.commit()
        except OperationTimeout:
            # Retry with even smaller chunks
            smaller_chunks = split_batch(chunk, 100)
            # ... continue recursively

Go

import "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/types"

events := generateLargeEventList()

// Original batch failed - split and retry
chunks := types.SplitGeoEventBatch(events, 500)

for _, chunk := range chunks {
    _, err := client.InsertEvents(chunk)
    if errors.Is(err, errors.ErrTimeout) {
        // Retry with smaller chunks
        smallerChunks := types.SplitGeoEventBatch(chunk, 100)
        // ... continue recursively
    }
}

Idempotency Guarantees

Server-Side Deduplication

The server maintains client sessions and deduplicates requests based on client_id + request_number. When retrying:

  1. SDK uses the same request_number for all retry attempts
  2. If the request already executed, server returns the cached response
  3. No double-execution occurs for idempotent operations

Idempotent vs Non-Idempotent Operations

Operation Idempotent Safe to Retry
upsert_events Yes Yes
query_* Yes Yes
delete_entities Yes Yes
insert_events No Use with caution

Recommendation: Prefer upsert_events over insert_events for safer retry behavior.

Session Recovery

If the client crashes between sending a request and receiving a response:

  • New client instance generates a new client_id
  • Cannot rely on deduplication from the old session
  • Application may need to handle potential duplicates

Retry Exhaustion

When all retry attempts are exhausted, the SDK returns a RetryExhausted error with:

  • Number of attempts made
  • The last error from the final attempt

Node.js

import { RetryExhausted } from 'archerdb-node'

try {
  await batch.commit()
} catch (error) {
  if (error instanceof RetryExhausted) {
    console.log(`Failed after ${error.attempts} attempts`)
    console.log(`Last error: ${error.lastError.message}`)
  }
}

Python

from archerdb import RetryExhausted

try:
    batch.commit()
except RetryExhausted as e:
    print(f"Failed after {e.attempts} attempts")
    print(f"Last error: {e.last_error}")

Go

import "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/retry"

events := generateLargeEventList()

err := retry.Do(func() error {
    _, err := client.InsertEvents(events)
    return err
}, config)

var exhausted retry.ErrRetryExhausted
if errors.As(err, &exhausted) {
    fmt.Printf("Failed after %d attempts\n", exhausted.Attempts)
    fmt.Printf("Last error: %v\n", exhausted.LastError)
}

Best Practices

  1. Use upsert over insert - Upsert operations are idempotent and safe to retry.

  2. Keep batches reasonably sized - Smaller batches (500-1000 events) have better retry characteristics than maximum-sized batches.

  3. Handle RetryExhausted - Always catch retry exhaustion and implement application-level fallback.

  4. Use split_batch for large imports - When importing large datasets, proactively split into chunks rather than waiting for timeouts.

  5. Monitor retry metrics - Track retry counts in production to detect cluster issues early.

  6. Don’t disable retry without reason - The default retry configuration handles most transient failures automatically.

  • Source: src/error_codes.zig
Edit this page

ArcherDB Error Codes Reference

This document provides a complete reference for all ArcherDB error codes.

Error Code Ranges

Range Category Description
0 Success Operation succeeded
1-99 Protocol Message format, checksums, version
100-199 Validation Invalid inputs, constraint violations
200-299 State Entity/cluster state errors
300-399 Resource Limits exceeded, capacity constraints
400-499 Security External security-boundary policy and access controls
500-599 Internal Bugs (should not occur in production)

Retry Semantics

Errors are classified into three categories:

  • Retryable: Transient errors that may succeed on retry (e.g., leader election, network issues)
  • Client Error: Invalid request that will always fail (fix the request, don’t retry)
  • Fatal: Server-side bugs (open an issue with logs and reproduction details)

Distributed Error Codes

Multi-Region Errors (213-218)

These errors occur in multi-region deployments with async replication.

Code Name Message Retryable
213 FOLLOWER_READ_ONLY Write operation rejected: follower regions are read-only No
214 STALE_FOLLOWER Follower data exceeds maximum staleness threshold Yes
215 PRIMARY_UNREACHABLE Cannot connect to primary region Yes
216 REPLICATION_TIMEOUT Cross-region replication timeout Yes
217 CONFLICT_DETECTED Write conflict detected in active-active replication No
218 GEO_SHARD_MISMATCH Entity geo-shard does not match target region No

Usage Notes:

  • Code 213: Writes must go to the primary region. SDKs automatically route writes to primary.
  • Code 214: The follower hasn’t caught up with replication. Wait and retry, or use a fresher replica.
  • Code 215: The primary region is down. Wait for failover or recovery.
  • Code 216: Cross-region replication is slow. Retry with backoff.
  • Code 217: Concurrent writes to the same entity detected in active-active replication. Application needs conflict resolution.
  • Code 218: Entity’s geo-shard doesn’t match the region handling the request. Check shard routing configuration.

Sharding Errors (220-224)

These errors occur in sharded cluster deployments.

Code Name Message Retryable
220 NOT_SHARD_LEADER This node is not the leader for target shard Yes
221 SHARD_UNAVAILABLE Target shard has no available replicas Yes
222 RESHARDING_IN_PROGRESS Cluster is currently resharding Yes
223 INVALID_SHARD_COUNT Target shard count is invalid No
224 SHARD_MIGRATION_FAILED Data migration to new shard failed No

Usage Notes:

  • Code 220: SDKs automatically refresh topology and retry. No application action needed.
  • Code 221: Wait for shard recovery. The cluster may be experiencing failures.
  • Code 222: Wait for resharding to complete. Operations will succeed after.
  • Code 223: The requested shard count is not valid (e.g., must be power of 2).
  • Code 224: A resharding operation failed. Check cluster health.

Security Boundary Errors (410-414, reserved/legacy)

These codes are reserved for deployments that layer external security controls around ArcherDB.

Code Name Message Retryable
410 ENCRYPTION_KEY_UNAVAILABLE External key service unavailable Yes
411 DECRYPTION_FAILED External data-protection validation failed No
412 ENCRYPTION_NOT_ENABLED External encryption policy not satisfied No
413 KEY_ROTATION_IN_PROGRESS External key rotation in progress Yes
414 UNSUPPORTED_ENCRYPTION_VERSION Unsupported external data-protection format/version No

Usage Notes:

  • Code 410: Check external key management service availability and IAM/policy bindings.
  • Code 411: Validate storage snapshot integrity and external decryption path.
  • Code 412: Verify infrastructure policy requires encrypted storage/transport for this route.
  • Code 413: Retry after external key rotation completes.
  • Code 414: Align external tooling format/version with deployment standards.

SDK Error Handling

Python

from archerdb import (
    MultiRegionError,
    ShardingError,
    MultiRegionException,
    ShardingException,
    is_retryable,
)

try:
    result = client.query_radius(lat, lon, radius)
except ShardingException as e:
    if e.error == ShardingError.RESHARDING_IN_PROGRESS:
        # Wait and retry - cluster is resharding
        time.sleep(5)
        result = client.query_radius(lat, lon, radius)
    elif is_retryable(e.code):
        # Generic retry logic
        result = retry_with_backoff(lambda: client.query_radius(lat, lon, radius))
    else:
        raise  # Non-retryable error

Java

import com.archerdb.geo.ShardingError;
import com.archerdb.geo.ArcherDBException;

try {
    QueryResult result = client.queryRadius(lat, lon, radius);
} catch (ArcherDBException e) {
    ShardingError shardError = ShardingError.fromCode(e.getErrorCode());
    if (shardError != null && shardError.isRetryable()) {
        // Retry with backoff
    }
}

Go

import "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/errors"

result, err := client.QueryRadius(lat, lon, radius)
if err != nil {
    if archerErr, ok := err.(*errors.ArcherDBError); ok {
        if errors.IsShardingError(int(archerErr.Code)) {
            if errors.IsRetryable(int(archerErr.Code)) {
                // Retry with backoff
            }
        }
    }
}

Node.js/TypeScript

import {
    ShardingError,
    ShardingException,
    isShardingError,
    isRetryable,
} from 'archerdb';

try {
    const result = await client.queryRadius(lat, lon, radius);
} catch (e) {
    if (e instanceof ShardingException) {
        if (e.error === ShardingError.RESHARDING_IN_PROGRESS) {
            // Wait and retry
            await sleep(5000);
            result = await client.queryRadius(lat, lon, radius);
        }
    }
}

Troubleshooting Guide

Multi-Region Issues

Symptom Likely Cause Solution
All writes fail with 213 Connected to follower Configure SDK with primary region
Reads return stale data Replication lag Check read_staleness_ns header
215 errors during failover Primary down Wait for new primary election

Sharding Issues

Symptom Likely Cause Solution
Frequent 220 errors Topology cache stale Reduce topology_refresh_interval
221 errors cluster-wide Shard failure Check cluster health, may need recovery
Long 222 wait times Large resharding Monitor resharding progress

Security Boundary Issues (410-414)

Symptom Likely Cause Solution
410 errors at startup External key service unreachable Check key-service connectivity and IAM/policy
411 errors on read External protection/integrity failure Restore from validated external snapshot
413 during rotation External key rotation window Wait for completion and retry
Edit this page

ArcherDB Testing Guide

Comprehensive guide for running ArcherDB tests locally across all 5 SDKs.

Overview

ArcherDB’s test suite covers:

  • Unit tests: Per-SDK operation validation
  • Integration tests: Multi-node cluster behavior
  • Parity tests: Cross-SDK result consistency
  • Edge case tests: Geographic boundary conditions
  • Performance tests: Latency and throughput benchmarks

Prerequisites

Required Software

Dependency Version Purpose
Python 3.11+ Test infrastructure, Python SDK
Node.js 20+ Node.js SDK tests
Go 1.21+ Go SDK tests
Java 21+ Java SDK tests (Maven included)
GCC/Clang Recent C SDK tests
Zig Bundled Core build and server tests

Installation

# Python test infrastructure
pip install -r test_infrastructure/requirements.txt

# Node.js SDK dependencies
cd src/clients/node && npm install

# Go SDK dependencies
cd src/clients/go && go mod download

# Java SDK dependencies
cd src/clients/java && mvn dependency:resolve

# C SDK - no external dependencies (header-only)
# Zig - bundled in repo at ./zig/zig (for server build/tests)

Quick Start

1. Build the Server

# Constrained build (recommended for most machines)
./zig/zig build -j4 -Dconfig=lite

# Full build (CI or dedicated machine)
./zig/zig build

2. Start a Local Server

# Single node for development
./zig/zig build run -- --port 3001

# Or run the pre-built binary
./zig-out/bin/archerdb --port 3001

3. Run SDK Tests

Each SDK has its own test suite. Run from the repository root:

Python:

cd src/clients/python
pip install pytest
pytest tests/ -v

Node.js:

cd src/clients/node
npm install
npm test

Go:

cd src/clients/go
go test ./... -v

Java:

cd src/clients/java
mvn test

C:

cd src/clients/c
make test

Server (Zig unit tests):

./zig/zig build -j4 -Dconfig=lite test:unit

4. Run Specific Test Filter

Most test frameworks support filtering:

# Python
pytest tests/ -v -k "insert"

# Go
go test ./... -v -run TestInsert

# Server (Zig)
./zig/zig build -j4 -Dconfig=lite test:unit -- --test-filter "insert"

Test Infrastructure

The test_infrastructure/ directory provides Python utilities for cluster management and test data generation.

Cluster Harness

Start and manage multi-node ArcherDB clusters programmatically:

from test_infrastructure.harness import ArcherDBCluster, ClusterConfig

# Start a 3-node cluster
config = ClusterConfig(node_count=3)
with ArcherDBCluster(config) as cluster:
    cluster.wait_for_ready(timeout=60)
    leader_addr = cluster.get_leader_address()
    # Run tests against leader_addr...

Data Generators

Generate test datasets with various distribution patterns:

from test_infrastructure.generators import generate_events, DatasetConfig

# Generate 1000 events concentrated around cities
events = generate_events(DatasetConfig(
    size=1000,
    pattern='city_concentrated',
    cities=['san_francisco', 'tokyo'],
    seed=42,  # Reproducible
))

See test_infrastructure/README.md for complete documentation.

Fixtures

Pre-defined test fixtures are in test_infrastructure/fixtures/v1/:

Fixture Size Use Case
smoke.json 10 events Quick connectivity tests
pr.json 100 events PR validation
nightly.json 1000 events Comprehensive testing

Environment Variables

Variable Description Default
ARCHERDB_HOST Server hostname 127.0.0.1
ARCHERDB_PORT Server port 3001
ARCHERDB_INTEGRATION Enable integration tests "" (disabled)
PRESERVE_ON_FAILURE Keep cluster data after failures "" (cleanup)
ARCHERDB_BIN Path to archerdb binary Auto-detect

Running Integration Tests

Integration tests require a running cluster and are gated by environment variable:

# Start cluster first
./zig/zig build run -- --port 3001

# Enable integration tests
export ARCHERDB_INTEGRATION=1
pytest tests/ -v -m integration

Parity Testing

Verify that all SDKs produce identical results:

# Run full parity suite
python tests/parity_tests/parity_runner.py

# Run specific operations
python tests/parity_tests/parity_runner.py --ops insert query-radius

# Run specific SDKs
python tests/parity_tests/parity_runner.py --sdks python node go

# Verbose output
python tests/parity_tests/parity_runner.py -v

Results are written to:

  • reports/parity.json - Machine-readable
  • docs/PARITY.md - Human-readable matrix

See docs/PARITY.md for methodology and current status.

Edge Case Testing

Geographic edge cases (poles, antimeridian, equator) are tested separately:

# Run edge case tests
pytest tests/edge_case_tests/ -v

# Run specific category
pytest tests/edge_case_tests/ -v -k "polar"
pytest tests/edge_case_tests/ -v -k "antimeridian"

Troubleshooting

Server Won’t Start

  1. Check if binary exists:

    ls zig-out/bin/archerdb
  2. Build if missing:

    ./zig/zig build -j4 -Dconfig=lite
  3. Check for port conflicts:

    lsof -i :3001

Tests Fail with Connection Errors

  1. Verify server is running:

    curl http://127.0.0.1:3001/ping
    # Should return: {"pong":true}
  2. Check environment variables:

    echo $ARCHERDB_HOST $ARCHERDB_PORT

Python Import Errors

Ensure test infrastructure is in path:

export PYTHONPATH="${PYTHONPATH}:${PWD}/test_infrastructure"

Or install in development mode:

pip install -e test_infrastructure/

Out of Memory During Tests

Use constrained build configuration:

# Instead of full build
./zig/zig build -j4 -Dconfig=lite test:unit

# Or minimal for low-memory systems
./zig/zig build -j2 -Dconfig=lite test:unit

Preserving Test Data for Debugging

export PRESERVE_ON_FAILURE=1
pytest tests/
# Data preserved in /tmp/archerdb-test-*

Resource-Constrained Testing

For machines with limited resources (24GB RAM, 8 cores):

Profile Command RAM Use Case
Minimal -j2 -Dconfig=lite ~2GB Heavy server load
Constrained -j4 -Dconfig=lite ~4GB Normal development
Full (default) ~8GB+ CI or dedicated machine

Use the helper script:

./scripts/test-constrained.sh unit              # Default: -j4, lite
./scripts/test-constrained.sh --minimal unit    # Minimal: -j2, lite
./scripts/test-constrained.sh --full unit       # Full resources
./scripts/test-constrained.sh check             # Quick compile check

CI Integration

Tests are run automatically in CI with tiered execution:

  • Smoke (<5 min): Every push, basic connectivity
  • PR (<15 min): Pull requests, full SDK suite
  • Nightly (2h): Manual-dispatch comprehensive multi-node testing
  • Weekly (3h): Manual-dispatch benchmark publication

See docs/testing/ci-tiers.md for tier details.

See Also


Last updated: 2026-02-01

Edit this page

CI Tier Structure

ArcherDB uses a tiered CI approach to balance fast feedback with comprehensive testing.

Overview

Tier Duration Trigger Purpose
Smoke <5 min Every push Fast feedback, gate PRs
PR <15 min Pull requests Comprehensive validation
Nightly 2h Manual dispatch Full coverage, edge cases
Weekly 3h Manual dispatch Performance regression detection and history publication

Tier 1: Smoke Tests

Trigger: Every push to main, every commit in PRs

Duration: <5 minutes

Scope:

  • Build verification (compiles cleanly)
  • Basic connectivity test per SDK
  • Single operation per SDK (insert + query)
  • Single-node topology only

Purpose:

  • Immediate feedback on breakage
  • Gate for PR merges
  • Catch obvious regressions fast

Failure handling:

  • Blocks PR merge
  • Notifies author immediately
  • Must fix before proceeding

Jobs:

Job Tests Time
build Compile check 30s
python-smoke 3 tests 45s
node-smoke 3 tests 45s
go-smoke 3 tests 30s
java-smoke 3 tests 90s
c-smoke 3 tests 20s

All SDK jobs run in parallel using GitHub Actions matrix strategy.

Tier 2: PR Tests

Trigger: Pull request opened, synchronized, or reopened

Duration: <15 minutes

Scope:

  • Full SDK test suite for all 5 SDKs
  • Single-node topology
  • All 14 operations tested
  • Error handling verification
  • Retry logic validation

Purpose:

  • Comprehensive validation before merge
  • Catch edge cases smoke tests miss
  • Verify parity across SDKs

Failure handling:

  • Blocks PR merge
  • Detailed test report in PR comment
  • Must fix all failures before merge

Jobs:

Job Tests Time
python-full ~100 tests 3 min
node-full ~80 tests 2 min
go-full ~70 tests 1.5 min
java-full ~60 tests 4 min
c-full ~50 tests 1 min
zig-full ~50 tests 1 min
parity-check 84 cells 3 min

Tier 3: Nightly Tests

Trigger: Manual workflow_dispatch

Duration: ~2 hours

Scope:

  • All Tier 2 tests plus:
  • Multi-node topologies (1, 3, 5, 6 nodes)
  • Geographic edge cases (poles, antimeridian, equator)
  • Failure injection tests
  • Recovery verification
  • Long-running stability tests

Purpose:

  • Catch topology-specific issues
  • Verify multi-node consistency
  • Test failure recovery paths
  • Find rare race conditions

Failure handling:

  • Does NOT block merges
  • Used for release-candidate and deep validation runs
  • Failures are triaged manually from workflow output and artifacts

Jobs:

Job Description Time
topology-1 Single node, all SDKs 15 min
topology-3 3-node cluster, all SDKs 25 min
topology-5 5-node cluster, all SDKs 30 min
topology-6 6-node cluster, all SDKs 35 min
edge-cases Geographic boundaries 10 min
failure-injection Node failures, partitions 20 min
stability Long-running workload 15 min

Tier 4: Weekly Benchmarks

Trigger: Manual workflow_dispatch

Duration: ~3 hours

Scope:

  • Full benchmark suite across topologies
  • Throughput benchmarks (events/sec)
  • Read latency (P50, P95, P99)
  • Write latency (P50, P95, P99)
  • Mixed workload benchmarks
  • SDK parity benchmarks

Purpose:

  • Detect performance regressions
  • Track performance trends
  • Compare SDK performance
  • Validate scaling behavior

Failure handling:

  • Used when maintainers want to refresh published benchmark history
  • Results are reviewed manually before promotion into long-term history
  • Does not block future merges

Performance Targets:

Metric Target Alert Threshold
3-node throughput >=770K events/sec -10%
Read latency P95 <1ms +20%
Read latency P99 <10ms +20%
Write latency P95 <10ms +20%
Write latency P99 <50ms +20%

Regression Detection:

  • Uses Welch’s t-test for statistical significance
  • Compares against stored baseline (JSON)
  • 95% confidence level (alpha=0.05)
  • Requires consistent CV <10% before comparison

Jobs:

Job Description Time
benchmark-1 Single node 30 min
benchmark-3 3-node cluster 45 min
benchmark-5 5-node cluster 50 min
benchmark-6 6-node cluster 55 min
sdk-parity-bench SDK comparison 20 min

Local benchmark outputs live under reports/benchmarks/, reports/history/, and reports/baselines/. Published history can be promoted into benchmarks/history/YYYY-MM-DD.json by the manual benchmark publication workflow.

Hardware

Tier Runner Specs
Smoke ubuntu-latest 2 cores, 7GB RAM
PR ubuntu-latest 2 cores, 7GB RAM
Nightly ubuntu-latest-4-cores 4 cores, 16GB RAM
Weekly ubuntu-latest-8-cores 8 cores, 32GB RAM

Artifacts

All tiers upload artifacts for debugging:

Artifact Retention Contents
test-reports 14 days JUnit XML, pytest output
coverage 14 days Coverage HTML, lcov data
logs 7 days Server logs, stderr
benchmarks 90 days JSON results, CSV data

Running Locally

Simulate each tier locally:

Smoke:

./scripts/test-constrained.sh check
pytest tests/ -v -m smoke --timeout=60

PR:

./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300

Nightly (requires cluster setup):

export ARCHERDB_INTEGRATION=1
python -m test_infrastructure.harness.cli start --nodes=3
pytest tests/ -v -m "integration or nightly"
python -m test_infrastructure.harness.cli stop

Weekly (requires cluster setup):

python3 test_infrastructure/benchmarks/cli.py run --full-suite

Workflow Files

CI workflows are defined in .github/workflows/:

File Tier
sdk-smoke.yml Smoke tests
sdk-pr.yml PR tests
sdk-nightly.yml Nightly tests
benchmark-weekly.yml Benchmark publication

Monitoring

CI health dashboard: Track test stability, flakiness, and duration trends.

Key metrics:

  • Pass rate by tier (target: >99% for smoke/PR)
  • Mean duration by tier
  • Flaky test count (target: 0)
  • Regression frequency (weekly benchmark)

See Also


Last updated: 2026-02-01

Edit this page

Performance Baseline Management

This document describes how ArcherDB manages performance baselines for regression detection in CI.

Overview

Performance baselines are locked reference points that CI uses to detect regressions. Each PR’s benchmark results are compared against the current main branch baseline. Regressions block merge to prevent shipping slow code.

Regression Thresholds

The following thresholds are used to detect performance regressions:

Metric Threshold Rationale
Throughput 5% Matches observed 5% coefficient of variation (CV) in benchmarks
Latency P99 25% Accounts for higher variance in tail latencies

Throughput: If current mean execution time is >5% slower than baseline, the check fails.

  • Formula: current_mean > baseline_mean * 1.05
  • Example: Baseline 1000ns, current 1060ns = 6% slower = FAIL

Latency P99: If current P99 latency is >25% higher than baseline, the check fails.

  • Formula: current_p99 > baseline_p99 * 1.25
  • Example: Baseline P99 1200ns, current P99 1400ns = 16.7% higher = PASS
  • Example: Baseline P99 1200ns, current P99 1600ns = 33.3% higher = FAIL

Baseline Lifecycle

main branch push
       |
       v
+------------------+
| Run benchmarks   |
| (full mode)      |
+------------------+
       |
       v
+------------------+
| Upload as        |
| benchmark-       |
| baseline         |
+------------------+
       |
       v
  (90 day retention)
PR created/updated
       |
       v
+------------------+
| Download         |
| baseline from    |
| main             |
+------------------+
       |
       v
+------------------+
| Run benchmarks   |
| (quick mode)     |
+------------------+
       |
       v
+------------------+
| Compare against  |
| baseline         |
+------------------+
       |
    +--+--+
    |     |
 PASS   FAIL
    |     |
    v     v
 Merge  Block
 OK     merge

Timeline

  1. Main branch pushes upload new baseline artifact (full benchmark mode)
  2. PRs download the current main baseline and run quick benchmarks
  3. Comparison checks both throughput and P99 latency against thresholds
  4. Regressions block merge until fixed or baseline is reset

Resetting the Baseline

Sometimes you need to reset the baseline after intentional performance changes:

When to Reset

  • Intentional trade-off: You accepted slower writes for better consistency
  • New feature overhead: Added necessary functionality that increases latency
  • Algorithm change: Changed from O(n) to O(log n) with different constants

How to Reset

  1. Delete the current baseline artifact:

    • Go to GitHub Actions > Select a recent main workflow run
    • Find “benchmark-baseline” artifact and delete it
    • Or use GitHub CLI: gh api -X DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}
  2. Merge your PR (no comparison runs without baseline)

  3. New baseline created on next main push

Alternative: Update Baseline Manually

If you don’t want to delete the artifact:

  1. Merge to main (workflow runs)
  2. New baseline automatically uploaded
  3. Future PRs compare against new baseline

Troubleshooting

False Positives

Symptom: Benchmark fails but code hasn’t changed performance.

Possible causes:

  • Stale baseline: If baseline is very old, machine differences may cause variance
  • CI runner variance: GitHub Actions runners can have different performance
  • Background load: Other jobs running on same machine

Resolution:

  • Re-run the benchmark job
  • If consistent, reset the baseline

Consistent Failures

Symptom: Multiple re-runs show same regression.

This likely indicates a real regression:

  1. Review recent commits for performance-impacting changes
  2. Profile locally to identify hot paths
  3. Fix the performance issue
  4. If intentional, reset the baseline and document why

No Baseline Available

Symptom: “Download baseline” step shows “Artifact not found”.

This is normal for:

  • First run after repository setup
  • After baseline was manually deleted
  • Baseline artifact expired (90 day retention)

Resolution: Merge to main to create new baseline.

jq/bc Not Available

Symptom: “jq not installed” warning in comparison output.

Resolution: The workflow installs these dependencies. If you’re running locally, install them:

# Ubuntu/Debian
sudo apt-get install jq bc

# macOS
brew install jq bc

Configuration

Modifying Thresholds

Thresholds are defined in scripts/benchmark-ci.sh:

# Throughput: 5% threshold
throughput_threshold=$(echo "scale=0; $baseline_mean * 1.05 / 1" | bc)

# Latency P99: 25% threshold
latency_threshold=$(echo "scale=0; $baseline_p99 * 1.25 / 1" | bc)

To change thresholds:

  1. Edit the multipliers in benchmark-ci.sh
  2. Update this documentation
  3. Update workflow header comments

Benchmark Modes

Mode Duration Use Case
quick ~30 seconds PRs (fast feedback)
full ~5 minutes Main branch (accurate baseline)
  • .github/workflows/benchmark.yml - CI workflow
  • scripts/benchmark-ci.sh - Benchmark runner and comparison logic
  • .planning/phases/09-testing-infrastructure/09-CONTEXT.md - Threshold decisions

References

  • .planning/phases/09-testing-infrastructure/09-CONTEXT.md - Why these thresholds were chosen
  • .planning/STATE.md - Observed 5% CV in benchmarks
Edit this page

ArcherDB Benchmark Guide

Guide for running, interpreting, and tracking ArcherDB performance benchmarks.

Overview

ArcherDB benchmarks measure:

  • Throughput: Events processed per second
  • Read latency: Query response time percentiles
  • Write latency: Insert response time percentiles
  • Mixed workload: Combined read/write performance
  • Scaling: Performance across topologies (1/3/5/6 nodes)

Performance Targets

The repository uses the following comparison gates on comparable hardware profiles:

Metric Baseline Target Stretch Target
3-node throughput >=770K events/sec >=1M events/sec
Read latency P95 <1ms <0.5ms
Read latency P99 <10ms <5ms
Write latency P95 <10ms <5ms
Write latency P99 <50ms <25ms

Running Benchmarks Locally

Prerequisites

# Install benchmark dependencies
pip install -r test_infrastructure/requirements.txt

# Build ArcherDB (lite config for testing)
./zig/zig build -j4 -Dconfig=lite

Quick Run (Single Topology)

# Run all benchmark types on 3-node cluster
python3 test_infrastructure/benchmarks/cli.py run --topology 3

# With time limit
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60

# With operation count limit
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --op-count 10000

Full Suite (All Topologies)

# Run complete benchmark suite (1/3/5/6 node topologies)
python3 test_infrastructure/benchmarks/cli.py run --full-suite

# Exclude mixed workload tests (faster)
python3 test_infrastructure/benchmarks/cli.py run --full-suite --no-mixed

Mixed Workload Benchmarks

Control the read/write ratio:

# 80% reads, 20% writes (default)
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.8

# 50% reads, 50% writes
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.5

# Write-heavy (20% reads, 80% writes)
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.2

# Read-only
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 1.0

Compare to Baseline

# Compare current run to stored baseline
python3 test_infrastructure/benchmarks/cli.py compare baseline.json current.json

Interpreting Results

Throughput

Events processed per second. Higher is better.

Throughput: <measured events/sec>
Compare against: the latest checked-in baseline for the same hardware/profile
Status: PASS when the run meets or exceeds the local comparison gate

Key factors:

  • Batch size (larger batches = higher throughput)
  • Network latency (lower = higher throughput)
  • Node count (more nodes = higher total throughput, but overhead)

Latency Percentiles

Query response times. Lower is better.

Read Latency:
  P50: 0.3ms  (median)
  P95: 0.8ms  (95% of requests)
  P99: 4.2ms  (99% of requests)

Target: P95 <1ms, P99 <10ms
Status: PASS

Percentile meanings:

  • P50 (median): Typical user experience
  • P95: 95% of requests are this fast or faster
  • P99: Captures tail latency, important for SLAs

Confidence Intervals

All means are reported with 95% confidence intervals:

P95: 0.8ms +/- 0.1ms (95% CI)

Narrower intervals = more stable measurements. Wide intervals suggest:

  • Insufficient samples
  • High variance in measurements
  • System noise

Coefficient of Variation (CV)

Measures result stability:

CV: 8.2% (target: <10%)
  • <10%: Results are stable, trustworthy
  • 10-20%: Somewhat noisy, consider more samples
  • >20%: High variance, investigate system state

Regression Detection

Threshold

A regression is detected when:

  • Performance degrades by >10% from baseline
  • Statistical test confirms significance (p < 0.05)

Statistical Method

We use Welch’s t-test (unequal variance):

from scipy.stats import ttest_ind
t_stat, p_value = ttest_ind(baseline, current, equal_var=False)

Benefits:

  • Does not assume equal variance between runs
  • Robust to different sample sizes
  • Standard statistical rigor

Comparison Report

Regression Analysis
==================
Baseline: previous checked-in artifact
Current:  current run artifact

Change: <measured delta>
p-value: <measured significance>
Status: REGRESSION DETECTED when the comparison crosses the configured threshold

Recommendation: Investigate recent changes when the current run underperforms the baseline

Historical Tracking

Manual Publication Runs

Maintainers can run the publication workflow manually and promote benchmark results into checked-in history:

benchmarks/history/
  2026-01-05.json
  2026-01-12.json
  2026-01-19.json
  2026-01-26.json
  2026-02-02.json
  ...

Baseline Files

Local baselines for regression detection live under:

reports/baselines/
  baseline-1node-*.json
  baseline-3node-*.json
  baseline-5node-*.json
  baseline-6node-*.json

Visualization

Results are visualized using github-action-benchmark:

  • Throughput graph: Events/sec over time
  • Latency graph: P95/P99 over time
  • Scaling graph: Performance vs node count

View at: https://github.com/[org]/archerdb/benchmarks

CI Integration

Publication Workflow

The benchmark-weekly.yml workflow:

  1. Spins up clusters (1/3/5/6 nodes)
  2. Runs full benchmark suite
  3. Compares to baseline
  4. Alerts on >10% regression
  5. Can promote approved results into benchmarks/history/
  6. Updates benchmark graphs

Alerts

On regression detection:

  • Workflow fails (visible in GitHub)
  • Comment posted on triggering commit
  • GitHub issue created with details
  • Slack notification (if configured)

Manual Trigger

# Trigger weekly benchmark manually
gh workflow run benchmark-weekly.yml

Programmatic Usage

from test_infrastructure.benchmarks import BenchmarkOrchestrator, BenchmarkConfig

# Create orchestrator
orchestrator = BenchmarkOrchestrator()

# Configure benchmark
config = BenchmarkConfig(
    topology=3,
    time_limit_sec=60,
    op_count_limit=10_000,
    read_write_ratio=0.8,  # 80% reads, 20% writes
)

# Run individual benchmarks
throughput = orchestrator.run_throughput_benchmark(3, config)
read_latency = orchestrator.run_latency_read_benchmark(3, config)
write_latency = orchestrator.run_latency_write_benchmark(3, config)
mixed = orchestrator.run_mixed_workload_benchmark(3, config)

# Access results
print(f"Throughput: {throughput['throughput_events_per_sec']}")
print(f"Read P95: {read_latency['p95_ms']}ms")
print(f"Write P95: {write_latency['p95_ms']}ms")

# Run full suite
results = orchestrator.run_full_suite(
    topologies=[1, 3, 5, 6],
    include_mixed=True,
)

Output Formats

Format Location Purpose
JSON reports/benchmarks/*.json CI automation, data processing
CSV reports/benchmarks/*.csv Spreadsheet analysis
Terminal stdout Interactive feedback
Markdown Updated docs Human review

JSON Format

{
  "timestamp": "2026-02-01T02:00:00Z",
  "topology": 3,
  "throughput": {
    "events_per_sec": 823456,
    "target": 770000,
    "passed": true
  },
  "read_latency": {
    "p50_ms": 0.3,
    "p95_ms": 0.8,
    "p99_ms": 4.2,
    "samples": 10000
  },
  "write_latency": {
    "p50_ms": 2.1,
    "p95_ms": 8.5,
    "p99_ms": 42.3,
    "samples": 2000
  },
  "metadata": {
    "version": "1.0.0",
    "git_sha": "abc1234",
    "runner": "ubuntu-latest-8-cores"
  }
}

Best Practices

Consistent Environment

  • Use dedicated hardware or CI runners
  • Close other applications during local runs
  • Use the same build configuration tier (for example, lite vs standard)

Warm-up

SDKs with JIT compilation (Java, Node.js) need warm-up:

SDK Recommended Warm-up
Java 500 iterations
Node.js 200 iterations
Python 100 iterations
Go 100 iterations
C 50 iterations

Sample Size

  • Minimum 1000 samples for percentile accuracy
  • Continue until CV < 10% (stability check)
  • Maximum 10 stability check rounds

Fresh Cluster

Each benchmark run should use a fresh cluster to ensure isolated measurements without accumulated state affecting results.

Troubleshooting

Results Vary Widely

  • Increase sample count
  • Check for background processes
  • Verify network stability
  • Use constrained build (-Dconfig=lite)

Benchmarks Hang

  • Check server health: curl http://localhost:3001/ping
  • Verify cluster formed: curl http://localhost:3001/topology
  • Check logs for errors

Results Don’t Match CI

  • Use same hardware profile as CI
  • Use same build configuration
  • Account for warm-up differences

See Also


Last updated: 2026-02-01

Edit this page

Single-Node Benchmark Evidence — August 2026

This document records a measured, reproducible single-node comparison of ArcherDB against Valkey and PostGIS on identical hardware, using the repo’s own harnesses. Raw outputs live in reports/benchmarks/evidence-20260803/.

Headline (single node, all ArcherDB writes durable + checksummed):

System Configuration Insert throughput
ArcherDB (native client, max batches) durable, consensus-committed 831,000 events/s
ArcherDB (Python SDK, pipelined) durable, consensus-committed 659,000 events/s
Valkey 8.1 GEOADD (best of any pipelining) no persistence 174,000–202,000 ops/s
Valkey 8.1 GEOADD (appendfsync always, pipelined) durable per op 109,000 ops/s
Valkey 8.1 GEOADD (appendfsync always, sequential) durable per op 1,474 ops/s
PostGIS 16 (GIST, batched inserts) durable, no consensus 29,000–31,000 rows/s

ArcherDB’s durable, replicable write path is 4–4.8× faster than Valkey’s volatile GEOADD ceiling, 7.6× faster than Valkey configured for comparable durability, and ~27× faster than PostGIS — while also serving spatial queries with sub-millisecond p99 latency.

Test environment

  • AMD EPYC (KVM guest), 8 vCPUs, 24 GB RAM, Linux 6.8.0-124-generic.
  • Virtio disk: ~571 µs per 4 KiB O_DIRECT+O_DSYNC write (7.2 MB/s), 901 MB/s for 1 MiB durable writes. Datacenter NVMe with power-loss protection has 10–20× lower sync-write latency, which disproportionately improves ArcherDB’s small-batch and Valkey’s fsync-always numbers.
  • ArcherDB: -Drelease -Dconfig=lite, single replica, O_DIRECT + O_DSYNC WAL (two durable writes per request), Aegis-128L checksums on every message. The lite tier shares the 10 MiB message envelope with all production tiers.
  • Valkey 8.1.9: official docker image, loopback inside the container netns. Default configuration (--save '' --appendonly no, io-threads=1) unless noted; the fsync rows use appendonly yes + appendfsync always.
  • PostGIS 16-3.4: official docker image, schema + GIST index from scripts/competitor-benchmarks/setup-postgis.sh, psycopg2.execute_values batched inserts.
  • Runs are serial; each measurement waits for system load < 2.0.

ArcherDB: insert throughput vs. request batch size

Native load generator (archerdb benchmark, in-process VSR client, one request in flight per client). Every request is consensus-committed and durable on disk before its reply.

Events per request Clients Events Throughput
240 1 200K 182,586 events/s
1,000 1 500K 344,232 events/s
8,000 1 1M 620,143 events/s
8,000 8 1M 637,246 events/s
65,535 1 1M 661,747 events/s
81,916 (max) 1 1M 901,823 events/s
81,916 (max) 1 2M 830,677 events/s

Reading the curve:

  • Small requests are bounded by the durable-write floor: each request pays two serialized O_DIRECT+O_DSYNC writes (~1.2 ms on this disk) regardless of size. Batching amortizes that cost; this is the design point, not a trick — one 10 MiB request carries up to 81,916 128-byte events.
  • At large batches the single replica core becomes execute-bound (~1 µs/event: S2 cell computation, cuckoo RAM-index upsert, LSM insert).
  • Concurrent clients neither help nor hurt materially at equal event totals (8,000-event batches: 620K/s at 1 client vs 637K/s at 8) — single-node writes are intentionally sequential in the WAL for crash-recovery correctness. Sustained multi-million-event runs shed ~8% (max batches) to ~25% (8K batches) to LSM compaction pacing; reducing per-commit compaction overhead at high commit rates is tracked as follow-up work.

ArcherDB: query latency (single node, 2M events loaded)

Measured in the same run as the 2M-event insert (N5-bmax-c1.txt):

Query p50 p99
UUID point lookup 92 µs 323 µs
UUID batch (per entity) 1 µs 3 µs
Radius (1 km) 91 µs 191 µs
Polygon 87 µs 327 µs

PostGIS on the same box and dataset shape: UUID lookup 3,461/s (~289 µs each, best case, no concurrency), radius 1,508/s (~663 µs), polygon 5.6/s (~178 ms).

Python SDK

All runs: fresh single-node server, 393K–400K events, zero errors.

Shape Throughput
benchmark_geo.py defaults (8,000-event requests, sequential) 421,282 events/s
65,535-event requests, sequential 475,540 events/s
insert_events_pipelined (8,000-event requests, window 8) 659,389 events/s
same shape, strictly synchronous 316,514 events/s

Pipelining (GeoClientSync.insert_events_pipelined / NativeClient.insert_events_pipelined) overlaps client-side packing with server round trips for a 2.1× gain over synchronous submission at the same batch size.

Before the August 2026 fixes the same harness measured 59–71K events/s: it capped requests at 240 events (a stale 32 KiB assumption) and the SDK built events one ctypes field at a time. If you have older numbers on file, they measured the harness, not the server.

Valkey comparison notes

  • GEOADD is Valkey’s closest analog to an ArcherDB event insert. Its ceiling on this box is ~174–202K ops/s regardless of pipelining depth or connection count (single-threaded server, ~5 µs/op); plain SET reaches 752K ops/s, which bounds any Valkey-based design from above.
  • Default Valkey acknowledges writes from memory only. A crash loses every write since the last RDB snapshot — with snapshots disabled (the default --save '' in many deployments), the entire dataset. ArcherDB replies only after the write is fsync’d and consensus-committed.
  • With appendfsync always (the durability-comparable configuration), Valkey drops to 109K ops/s pipelined — and 1,474 ops/s for sequential clients, because each op pays the same ~0.6 ms sync-write cost that ArcherDB amortizes across up to 81,916 events per request.

Reproduction

# ArcherDB native curve (fresh temp server per run):
./zig/zig build -Drelease -Dconfig=lite
./zig-out/bin/archerdb benchmark --event-count=1000000 --event-batch-size=8000
./zig-out/bin/archerdb benchmark --event-count=2000000   # max batches

# Python SDK (server on :3001):
./zig-out/bin/archerdb format --cluster=0 --replica=0 --replica-count=1 0_0.archerdb
./zig-out/bin/archerdb start --addresses=127.0.0.1:3001 0_0.archerdb &
python3 benchmark_geo.py --events 400000 --batch-size 8000 --addresses 127.0.0.1:3001

# Valkey (docker):
docker run -d --name valkey valkey/valkey:8.1 valkey-server --save '' --appendonly no
docker exec valkey valkey-benchmark -n 2000000 -c 8 -P 64 -r 10000000 -q \
  GEOADD fleet 13.361389 38.115556 m:__rand_int__

# PostGIS (docker):
docker run -d --name postgis -e POSTGRES_USER=bench -e POSTGRES_PASSWORD=bench \
  -e POSTGRES_DB=geobench -p 127.0.0.1:5433:5432 postgis/postgis:16-3.4
# schema: scripts/competitor-benchmarks/setup-postgis.sh
python3 scripts/competitor-benchmarks/benchmark-postgis.py --port 5433 \
  --event-count 200000 --batch-size 8000 --json

Caveats: single-node lite tier on shared virtualized hardware; a background load gate (< 2.0) was enforced but the host is not dedicated. Numbers on dedicated hardware with power-loss-protected NVMe will be higher, especially for small batches. Multi-node cluster benchmarks are tracked separately (docs/BENCHMARKS.md targets: ≥770K events/s on 3 nodes).

Edit this page

Release Checklist

Use this checklist before calling ArcherDB released for the current GA surface.

This checklist is intentionally separate from Maven Central publication of archerdb-java. Java Central distribution is a packaging step that should happen only after the release candidate itself is already a clear GO. See the Java Publish Checklist for that separate step.

Must-Do Before Release

All of the following must be true before ArcherDB is called released:

  • choose the exact release commit explicitly
  • ensure the worktree is clean for that release commit
  • ensure GitHub Actions are green on that exact commit:
    • CI
    • SDK Smoke Tests
    • Performance Benchmarks
  • refresh release evidence for the exact release commit, not an older nearby commit:
    • regenerate reports/parity.json
    • regenerate docs/PARITY.md
    • verify the checked-in benchmark artifact set still matches the chosen release commit
    • update integration-check-report.md so its date and commit match the release candidate
  • confirm the current GA boundary is still truthful in code and docs:
    • cluster is status-only
    • upgrade is status plus dry-run planning only
    • offline shard reshard remains planning-only and fail-closed without --dry-run
    • non-GA areas remain documented as non-GA rather than being implied as shipping features
  • confirm there are no repo-local correctness blockers left in FINALIZATION_PLAN.md beyond external release-evidence tasks
  • confirm release-facing docs and announcement material do not imply package distribution that has not happened yet

Current Non-GA Boundaries

These are intentionally outside the current GA release surface and should stay described that way:

  • dynamic cluster membership
  • live upgrade actuation beyond status and dry-run planning
  • server-side multi-region runtime
  • in-process CRL/OCSP revocation enforcement

Separate From Release Go/No-Go

The following is not part of the core ArcherDB release go/no-go decision:

  • publishing archerdb-java to Maven Central

That step should remain separate because it is a public distribution action, not a runtime correctness gate. If Java Central publication is deferred, the release decision can still be GO provided that release notes and docs do not claim the package is already available there.

Decision Rule

  • GO means the exact release commit is selected, green, evidence is refreshed to that commit, and the current GA boundary is still truthful.
  • NO-GO means any of those items are still open.

Current State On Main

As of April 15, 2026, these items are already satisfied on main:

  • the latest origin/main commit is green for CI, SDK Smoke Tests, and Performance Benchmarks
  • the current GA CLI boundary is truthful in the built binary
  • the intentional non-GA boundaries are documented as non-GA
  • release-facing docs no longer imply Maven Central distribution before it actually happens

These items are still open before calling ArcherDB released:

  • choose and freeze the exact release commit
  • refresh parity and integration evidence so it points at that exact release commit
Edit this page

Java Publish Checklist

Use this checklist before publishing com.archerdb:archerdb-java to Maven Central.

This checklist is separate from the main Release Checklist. ArcherDB should already be a clear release GO before you use this document.

Publishing to Central is a public release action, not a test step. Do not publish just to verify that Sonatype credentials work. If the answer to “should this exact commit become a public Java release?” is not clearly yes, stop here.

No-Go Conditions

Do not publish if any of these are true:

  • the candidate commit is not the exact commit you intend to expose publicly
  • main or the chosen release branch is still moving and you have not frozen the release target
  • any required GitHub Actions workflow is red or still running on the candidate commit
  • parity evidence is stale relative to the candidate commit
  • benchmark evidence is stale relative to the candidate commit
  • integration-check-report.md or FINALIZATION_PLAN.md still lists a repo-local correctness gap that should block release
  • the Java release build does not stage artifacts into zig-out/dist/java
  • the Java publish preflight does not pass on the release host
  • you are missing Central credentials or a usable GPG signing key

Go Criteria

Only publish when all of these are true:

  • the worktree is clean and the candidate commit is selected explicitly
  • the candidate commit is green in GitHub Actions for:
    • CI
    • SDK Smoke Tests
    • Performance Benchmarks
  • docs/PARITY.md and reports/parity.json are current and green for the candidate commit
  • docs/BENCHMARKS.md still matches the current checked-in benchmark artifact set
  • integration-check-report.md says the only remaining release-evidence work is the external Central rehearsal
  • the Java release build stages all four artifacts:
    • archerdb-java-<version>.jar
    • archerdb-java-<version>-sources.jar
    • archerdb-java-<version>-javadoc.jar
    • archerdb-java-<version>.pom
  • the release host has:
    • Java
    • Maven
    • MAVEN_USERNAME
    • MAVEN_CENTRAL_TOKEN
    • MAVEN_GPG_PASSPHRASE
    • a GPG secret key available to gpg --list-secret-keys

Minimal Pre-Publish Flow

Use the exact release commit, not “whatever is currently on main”:

git status -sb
git rev-parse HEAD

Build the scripts binary once:

./zig/zig build scripts:build -j2

Set the Java toolchain for the release host:

export JAVA_HOME=<your-jdk-home>
export PATH="$JAVA_HOME/bin:$PATH"

Stage the Java release artifacts:

./zig-out/bin/scripts release --sha=<commit> --language=java --build

Confirm the staged files exist:

ls -1 zig-out/dist/java

Set the publish credentials and signing passphrase:

export MAVEN_USERNAME=<central-token-username>
export MAVEN_CENTRAL_TOKEN=<central-token-password>
export MAVEN_GPG_PASSPHRASE=<gpg-passphrase>
gpg --list-secret-keys --keyid-format LONG

Run the no-side-effects publish preflight:

./zig-out/bin/scripts release --sha=<commit> --language=java --publish --preflight

Only if every earlier gate is green, do the real publish:

./zig-out/bin/scripts release --sha=<commit> --language=java --publish

Decision Rule

  • GO means every repo-side gate is green, the candidate commit is intentionally being released, and the publish preflight passes on the release host.
  • NO-GO means anything else. In that case, do not publish to Maven Central yet.
Edit this page

ArcherDB API Reference

This document provides complete API documentation for ArcherDB, covering all operations, data types, error handling, and protocol details.

Machine-readable API spec available at openapi.yaml

For language-specific examples, see the SDK overview and the README files under src/clients/.

Overview

ArcherDB provides a binary protocol over TCP for high-performance geospatial operations. For most use cases, we recommend using one of the official SDKs rather than implementing the protocol directly.

Consistency Model

ArcherDB uses Viewstamped Replication (VSR) to provide linearizability - all operations appear to execute atomically in a single, consistent order across all replicas. Once an operation returns success:

  • The data is durably stored on a majority of replicas
  • All subsequent reads will see the written data
  • The operation will survive any minority of replica failures

Request/Response Flow

  1. Client sends a request to any replica
  2. If the replica is not the primary, it forwards to the primary
  3. Primary replicates to followers and waits for quorum
  4. Primary commits and returns response to client

For more details on the consensus protocol, see VSR Understanding.

Data Types

GeoEvent

A GeoEvent represents a single location update for an entity (vehicle, device, user, etc.).

Field Type Range Description
id u128 Auto-generated Composite key (entity_id + timestamp). Do not set manually.
entity_id u128 Non-zero Unique identifier for the tracked entity
correlation_id u128 Any Trip, session, or job correlation ID
user_data u128 Any Application-specific metadata
lat_nano i64 -90e9 to +90e9 Latitude in nanodegrees
lon_nano i64 -180e9 to +180e9 Longitude in nanodegrees
group_id u64 Any Fleet, region, or tenant identifier
altitude_mm i32 -10,000,000 to +100,000,000 Altitude in millimeters (-10km to +100km)
velocity_mms u32 0 to 1,000,000,000 Speed in millimeters per second (0 to 1000 m/s)
ttl_seconds u32 0 to 4,294,967,295 Time-to-live in seconds (0 = never expire)
accuracy_mm u32 0 to 4,294,967,295 GPS accuracy radius in millimeters
heading_cdeg u16 0 to 35999 Heading in centidegrees (0 = North, 9000 = East)
flags u16 Bitmask Status flags (application-defined)

Coordinate Encoding

ArcherDB uses integer coordinates for precision and performance:

latitude_nanodegrees = latitude_degrees × 1,000,000,000
longitude_nanodegrees = longitude_degrees × 1,000,000,000
altitude_mm = altitude_meters × 1,000
velocity_mms = velocity_mps × 1,000
heading_cdeg = heading_degrees × 100

Example: San Francisco (37.7749, -122.4194) becomes:

  • lat_nano: 37,774,900,000
  • lon_nano: -122,419,400,000

Precision: Nanodegrees provide ~0.1mm precision at the equator, far exceeding GPS accuracy.

ID Types

Type Size Usage
u128 128 bits Entity IDs, correlation IDs, user data
u64 64 bits Group IDs, timestamps
u32 32 bits Limits, counters, durations

All SDKs provide an id() function to generate unique 128-bit identifiers.

Operations

createBatch / commit

Insert or upsert a batch of GeoEvents atomically.

Request

Field Type Required Description
events GeoEvent[] Yes Array of events to insert/upsert (1 to 10,000)
mode string No "insert" (default) or "upsert"

Insert vs Upsert:

  • Insert: Fails if an event with the same entity_id + timestamp exists
  • Upsert: Updates existing event or inserts if not present (idempotent, recommended)

Response

Field Type Description
results EventResult[] Per-event results (same order as request)
committed bool True if the batch committed successfully

EventResult:

Field Type Description
index u32 Index in the original batch
result u16 Result code (0 = success, see Error Codes)

Errors

Code Name Description Retryable
100 INVALID_COORDINATES Latitude or longitude out of valid range No
101 INVALID_ENTITY_ID Entity ID is zero No
300 BATCH_TOO_LARGE Batch exceeds 10,000 events No
211 CLUSTER_UNAVAILABLE No quorum available Yes
220 NOT_SHARD_LEADER Wrong shard (auto-retried by SDK) Yes

For complete error codes, see Error Codes Reference.

curl Example

# Insert two events for vehicles in San Francisco
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "entity_id": "550e8400-e29b-41d4-a716-446655440000",
        "lat_nano": 37774900000,
        "lon_nano": -122419400000,
        "group_id": 1,
        "ttl_seconds": 86400
      },
      {
        "entity_id": "550e8400-e29b-41d4-a716-446655440001",
        "lat_nano": 37784900000,
        "lon_nano": -122409400000,
        "group_id": 1,
        "ttl_seconds": 86400
      }
    ],
    "mode": "upsert"
  }'

SDK Examples

Node.js
import { createGeoClient, createGeoEvent, id } from 'archerdb-node'

const client = await createGeoClient({
  cluster_id: 0n,
  addresses: ['127.0.0.1:3000'],
})

// Create a batch
const batch = client.createBatch()

// Add events
batch.add(createGeoEvent({
  entity_id: id(),
  latitude: 37.7749,
  longitude: -122.4194,
  group_id: 1n,
}))

batch.add(createGeoEvent({
  entity_id: id(),
  latitude: 37.7849,
  longitude: -122.4094,
  group_id: 1n,
}))

// Commit (default: insert mode)
const results = await batch.commit()

// Check per-event results
for (const result of results) {
  if (result.error) {
    console.error(`Event ${result.index} failed: ${result.error}`)
  }
}

// Or use upsert mode (idempotent)
const upsertBatch = client.createBatch({ mode: 'upsert' })
// ... add events ...
await upsertBatch.commit()
Python
import archerdb

client = archerdb.GeoClientSync(archerdb.GeoClientConfig(
    cluster_id=0,
    addresses=['127.0.0.1:3000']
))

# Create a batch
batch = client.create_batch()

# Add events
batch.add(archerdb.create_geo_event(
    entity_id=archerdb.id(),
    latitude=37.7749,
    longitude=-122.4194,
    group_id=1,
))

batch.add(archerdb.create_geo_event(
    entity_id=archerdb.id(),
    latitude=37.7849,
    longitude=-122.4094,
    group_id=1,
))

# Commit
results = batch.commit()

# Check per-event results
for result in results:
    if result.error:
        print(f"Event {result.index} failed: {result.error}")
Go
import (
    archerdb "github.com/ArcherDB-io/archerdb/src/clients/go"
    "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/types"
)

client, err := archerdb.NewClient(types.ToUint128(0), []string{"127.0.0.1:3000"})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Create events
events := []types.GeoEvent{
    {
        EntityID: types.ID(),
        LatNano:  37774900000,
        LonNano:  -122419400000,
        GroupID:  1,
    },
    {
        EntityID: types.ID(),
        LatNano:  37784900000,
        LonNano:  -122409400000,
        GroupID:  1,
    },
}

// Insert events
results, err := client.CreateEvents(events)
if err != nil {
    log.Fatal(err)
}

// Check per-event results
for _, result := range results {
    if result.Result != 0 {
        log.Printf("Event %d failed: %v", result.Index, result.Result)
    }
}
Java
import com.archerdb.geo.*;
import java.math.BigInteger;
import java.util.List;

GeoClientConfig config = new GeoClientConfig.Builder()
    .clusterId(BigInteger.ZERO)
    .addresses(List.of("127.0.0.1:3000"))
    .build();

try (GeoClient client = new GeoClient(config)) {
    // Create events
    List<GeoEvent> events = List.of(
        GeoEvent.builder()
            .entityId(GeoClient.generateId())
            .latNano(37774900000L)
            .lonNano(-122419400000L)
            .groupId(1L)
            .build(),
        GeoEvent.builder()
            .entityId(GeoClient.generateId())
            .latNano(37784900000L)
            .lonNano(-122409400000L)
            .groupId(1L)
            .build()
    );

    // Insert events
    List<EventResult> results = client.createEvents(events);

    // Check per-event results
    for (EventResult result : results) {
        if (result.getResult() != 0) {
            System.err.println("Event " + result.getIndex() + " failed: " + result.getResult());
        }
    }
}
C
#include <archerdb.h>
#include <stdio.h>

arch_client_t* client = arch_client_new(0, "127.0.0.1:3000", NULL);

// Create events
geo_event_t events[2] = {
    {
        .entity_id = arch_id(),
        .lat_nano = 37774900000,
        .lon_nano = -122419400000,
        .group_id = 1,
    },
    {
        .entity_id = arch_id(),
        .lat_nano = 37784900000,
        .lon_nano = -122409400000,
        .group_id = 1,
    },
};

// Insert events (synchronous callback)
void on_result(void* ctx, const event_result_t* results, size_t count) {
    for (size_t i = 0; i < count; i++) {
        if (results[i].result != 0) {
            printf("Event %u failed: %u\n", results[i].index, results[i].result);
        }
    }
}

arch_create_events(client, events, 2, on_result, NULL);

arch_client_destroy(client);

queryRadius

Find all entities within a radius of a center point.

Request

Field Type Required Description
center_lat f64 Yes Center latitude in degrees (-90 to +90)
center_lon f64 Yes Center longitude in degrees (-180 to +180)
radius_m u32 Yes Radius in meters (1 to 40,000,000)
limit u32 No Maximum results per page (default: 1,000, max: 10,000)
cursor bytes No Pagination cursor from previous response
group_id u64 No Filter by group ID

Response

Field Type Description
events GeoEvent[] Matching events
has_more bool True if more results available
cursor bytes Cursor for next page (present if has_more is true)

Ordering: Results are returned in deterministic order based on S2 cell ID, enabling consistent pagination.

Errors

Code Name Description Retryable
100 INVALID_COORDINATES Center coordinates out of range No
101 INVALID_RADIUS Radius outside valid range (1 to 40,000,000 meters) No
300 QUERY_RESULT_TOO_LARGE Result set exceeds configured maximum No

curl Example

# Find all entities within 1km of downtown San Francisco
curl -X POST http://localhost:3000/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat": 37.7749,
    "center_lon": -122.4194,
    "radius_m": 1000,
    "limit": 100
  }'

# With group filter (only fleet 1)
curl -X POST http://localhost:3000/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat": 37.7749,
    "center_lon": -122.4194,
    "radius_m": 5000,
    "group_id": 1,
    "limit": 100
  }'

SDK Examples

Node.js
// Basic query
const results = await client.queryRadius({
  center_lat: 37.7749,
  center_lon: -122.4194,
  radius_m: 1000,
  limit: 100,
})

console.log(`Found ${results.events.length} entities`)

// With group filter
const fleetResults = await client.queryRadius({
  center_lat: 37.7749,
  center_lon: -122.4194,
  radius_m: 5000,
  group_id: 1n,  // Only fleet 1
  limit: 100,
})

// Pagination
let allEvents = []
let cursor = undefined

do {
  const page = await client.queryRadius({
    center_lat: 37.7749,
    center_lon: -122.4194,
    radius_m: 10000,
    limit: 1000,
    cursor,
  })
  allEvents.push(...page.events)
  cursor = page.has_more ? page.cursor : undefined
} while (cursor)
Python
# Basic query
results = client.query_radius(
    center_lat=37.7749,
    center_lon=-122.4194,
    radius_m=1000,
    limit=100,
)

print(f"Found {len(results.events)} entities")

# With group filter
fleet_results = client.query_radius(
    center_lat=37.7749,
    center_lon=-122.4194,
    radius_m=5000,
    group_id=1,  # Only fleet 1
    limit=100,
)

# Pagination
all_events = []
cursor = None

while True:
    page = client.query_radius(
        center_lat=37.7749,
        center_lon=-122.4194,
        radius_m=10000,
        limit=1000,
        cursor=cursor,
    )
    all_events.extend(page.events)
    if not page.has_more:
        break
    cursor = page.cursor
Go
// Basic query
filter := types.RadiusFilter{
    CenterLatNano: 37774900000,
    CenterLonNano: -122419400000,
    RadiusM:       1000,
    Limit:         100,
}

results, err := client.QueryRadius(filter)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Found %d entities\n", len(results.Events))

// Pagination
var allEvents []types.GeoEvent
var cursor []byte

for {
    filter := types.RadiusFilter{
        CenterLatNano: 37774900000,
        CenterLonNano: -122419400000,
        RadiusM:       10000,
        Limit:         1000,
        Cursor:        cursor,
    }

    page, err := client.QueryRadius(filter)
    if err != nil {
        log.Fatal(err)
    }

    allEvents = append(allEvents, page.Events...)
    if !page.HasMore {
        break
    }
    cursor = page.Cursor
}
Java
// Basic query
QueryRadiusFilter filter = new QueryRadiusFilter.Builder()
    .centerLatNano(37774900000L)
    .centerLonNano(-122419400000L)
    .radiusM(1000)
    .limit(100)
    .build();

QueryResult results = client.queryRadius(filter);
System.out.println("Found " + results.getEvents().size() + " entities");

// Pagination
List<GeoEvent> allEvents = new ArrayList<>();
byte[] cursor = null;

do {
    QueryRadiusFilter pageFilter = new QueryRadiusFilter.Builder()
        .centerLatNano(37774900000L)
        .centerLonNano(-122419400000L)
        .radiusM(10000)
        .limit(1000)
        .cursor(cursor)
        .build();

    QueryResult page = client.queryRadius(pageFilter);
    allEvents.addAll(page.getEvents());
    cursor = page.hasMore() ? page.getCursor() : null;
} while (cursor != null);
C
// Basic query
radius_filter_t filter = {
    .center_lat_nano = 37774900000,
    .center_lon_nano = -122419400000,
    .radius_m = 1000,
    .limit = 100,
};

void on_result(void* ctx, const geo_event_t* events, size_t count, bool has_more, const uint8_t* cursor, size_t cursor_len) {
    printf("Found %zu entities\n", count);

    // Handle pagination if needed
    if (has_more) {
        // Store cursor for next query
    }
}

arch_query_radius(client, &filter, on_result, NULL);

queryPolygon

Find all entities within a polygon boundary, optionally excluding holes.

Request

Field Type Required Description
vertices Coordinate[] Yes Outer boundary vertices (3 to 1,000 points, counter-clockwise)
holes Coordinate[][] No Interior holes to exclude (0 to 100 holes, clockwise winding)
limit u32 No Maximum results per page (default: 1,000, max: 10,000)
cursor bytes No Pagination cursor from previous response
group_id u64 No Filter by group ID

Coordinate:

Field Type Description
lat f64 Latitude in degrees (-90 to +90)
lon f64 Longitude in degrees (-180 to +180)

Winding Order:

  • Outer boundary: Counter-clockwise (exterior ring)
  • Holes: Clockwise (interior rings)

This follows the GeoJSON convention. SDKs provide validation helpers to check winding order.

Response

Field Type Description
events GeoEvent[] Matching events
has_more bool True if more results available
cursor bytes Cursor for next page (present if has_more is true)

Errors

Code Name Description Retryable
100 INVALID_COORDINATES Vertex coordinates out of range No
102 POLYGON_TOO_COMPLEX Too many vertices (max 1,000) or holes (max 100) No
103 INVALID_POLYGON Self-intersecting, degenerate, or invalid hole layout No

curl Example

# Find all entities within a rectangular area of downtown San Francisco
curl -X POST http://localhost:3000/query/polygon \
  -H "Content-Type: application/json" \
  -d '{
    "vertices": [
      {"lat": 37.79, "lon": -122.42},
      {"lat": 37.79, "lon": -122.39},
      {"lat": 37.76, "lon": -122.39},
      {"lat": 37.76, "lon": -122.42}
    ],
    "limit": 1000
  }'

SDK Examples

Node.js
// Simple polygon (downtown San Francisco)
const results = await client.queryPolygon({
  vertices: [
    { lat: 37.79, lon: -122.42 },  // NW
    { lat: 37.79, lon: -122.39 },  // NE
    { lat: 37.76, lon: -122.39 },  // SE
    { lat: 37.76, lon: -122.42 },  // SW
  ],
  limit: 1000,
})

// Polygon with hole (exclude a park)
const resultsWithHole = await client.queryPolygon({
  vertices: [
    { lat: 37.79, lon: -122.42 },
    { lat: 37.79, lon: -122.39 },
    { lat: 37.76, lon: -122.39 },
    { lat: 37.76, lon: -122.42 },
  ],
  holes: [
    // Park to exclude (clockwise winding)
    [
      { lat: 37.78, lon: -122.41 },
      { lat: 37.775, lon: -122.41 },
      { lat: 37.775, lon: -122.40 },
      { lat: 37.78, lon: -122.40 },
    ],
  ],
  limit: 1000,
})
Python
# Simple polygon
results = client.query_polygon(
    vertices=[
        (37.79, -122.42),  # NW
        (37.79, -122.39),  # NE
        (37.76, -122.39),  # SE
        (37.76, -122.42),  # SW
    ],
    limit=1000,
)

# Polygon with hole
results_with_hole = client.query_polygon(
    vertices=[
        (37.79, -122.42),
        (37.79, -122.39),
        (37.76, -122.39),
        (37.76, -122.42),
    ],
    holes=[
        # Park to exclude (clockwise winding)
        [
            (37.78, -122.41),
            (37.775, -122.41),
            (37.775, -122.40),
            (37.78, -122.40),
        ],
    ],
    limit=1000,
)
Go
// Simple polygon
vertices := [][]float64{
    {37.79, -122.42},
    {37.79, -122.39},
    {37.76, -122.39},
    {37.76, -122.42},
}

filter, err := types.NewPolygonQuery(vertices, 1000)
if err != nil {
    log.Fatal(err)
}

results, err := client.QueryPolygon(filter)

// Polygon with hole
parkHole := [][]float64{
    {37.78, -122.41},
    {37.775, -122.41},
    {37.775, -122.40},
    {37.78, -122.40},
}

filterWithHole, err := types.NewPolygonQuery(vertices, 1000, parkHole)
Java
// Simple polygon
QueryPolygonFilter filter = new QueryPolygonFilter.Builder()
    .addVertex(37.79, -122.42)
    .addVertex(37.79, -122.39)
    .addVertex(37.76, -122.39)
    .addVertex(37.76, -122.42)
    .setLimit(1000)
    .build();

QueryResult results = client.queryPolygon(filter);

// Polygon with hole
QueryPolygonFilter filterWithHole = new QueryPolygonFilter.Builder()
    .addVertex(37.79, -122.42)
    .addVertex(37.79, -122.39)
    .addVertex(37.76, -122.39)
    .addVertex(37.76, -122.42)
    .startHole()
    .addHoleVertex(37.78, -122.41)
    .addHoleVertex(37.775, -122.41)
    .addHoleVertex(37.775, -122.40)
    .addHoleVertex(37.78, -122.40)
    .finishHole()
    .setLimit(1000)
    .build();
C
// Simple polygon
coordinate_t vertices[] = {
    {.lat = 37.79, .lon = -122.42},
    {.lat = 37.79, .lon = -122.39},
    {.lat = 37.76, .lon = -122.39},
    {.lat = 37.76, .lon = -122.42},
};

polygon_filter_t filter = {
    .vertices = vertices,
    .vertex_count = 4,
    .limit = 1000,
};

arch_query_polygon(client, &filter, on_result, NULL);

getLatest

Get the most recent location for a single entity.

Request

Field Type Required Description
entity_id u128 Yes Entity to look up

Response

Field Type Description
event GeoEvent Most recent event (null if entity not found)
found bool True if entity exists

Errors

Code Name Description Retryable
101 INVALID_ENTITY_ID Entity ID is zero No

curl Example

# Get the latest location for an entity
curl http://localhost:3000/entity/550e8400-e29b-41d4-a716-446655440000

SDK Examples

Node.js
const event = await client.getLatest(entityId)

if (event) {
  console.log(`Last seen at: ${event.lat_nano / 1e9}, ${event.lon_nano / 1e9}`)
} else {
  console.log('Entity not found')
}
Python
event = client.get_latest(entity_id)

if event:
    print(f"Last seen at: {event.lat_nano / 1e9}, {event.lon_nano / 1e9}")
else:
    print("Entity not found")
Go
event, found, err := client.GetLatest(entityID)
if err != nil {
    log.Fatal(err)
}

if found {
    fmt.Printf("Last seen at: %f, %f\n", float64(event.LatNano)/1e9, float64(event.LonNano)/1e9)
} else {
    fmt.Println("Entity not found")
}
Java
Optional<GeoEvent> event = client.getLatest(entityId);

if (event.isPresent()) {
    GeoEvent e = event.get();
    System.out.printf("Last seen at: %f, %f%n",
        e.getLatNano() / 1e9, e.getLonNano() / 1e9);
} else {
    System.out.println("Entity not found");
}
C
geo_event_t event;
bool found = arch_get_latest(client, entity_id, &event);

if (found) {
    printf("Last seen at: %f, %f\n",
        (double)event.lat_nano / 1e9,
        (double)event.lon_nano / 1e9);
} else {
    printf("Entity not found\n");
}

getLatestBatch

Get the most recent location for multiple entities in a single request.

Request

Field Type Required Description
entity_ids u128[] Yes Entities to look up (1 to 10,000)

Response

Field Type Description
events GeoEvent[] Events for found entities (may be fewer than requested)

Note: The response only includes events for entities that exist. Missing entities are silently omitted.

curl Example

# Get latest locations for multiple entities
curl -X POST http://localhost:3000/entities/batch \
  -H "Content-Type: application/json" \
  -d '{
    "entity_ids": [
      "550e8400-e29b-41d4-a716-446655440000",
      "550e8400-e29b-41d4-a716-446655440001",
      "550e8400-e29b-41d4-a716-446655440002"
    ]
  }'

SDK Examples

Node.js
const events = await client.getLatestBatch([entityId1, entityId2, entityId3])

console.log(`Found ${events.length} of 3 entities`)

for (const event of events) {
  console.log(`Entity ${event.entity_id}: ${event.lat_nano}, ${event.lon_nano}`)
}
Python
events = client.get_latest_batch([entity_id1, entity_id2, entity_id3])

print(f"Found {len(events)} of 3 entities")

for event in events:
    print(f"Entity {event.entity_id}: {event.lat_nano}, {event.lon_nano}")
Go
entityIDs := []types.Uint128{entityID1, entityID2, entityID3}

events, err := client.GetLatestBatch(entityIDs)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Found %d of 3 entities\n", len(events))
Java
List<BigInteger> entityIds = List.of(entityId1, entityId2, entityId3);

List<GeoEvent> events = client.getLatestBatch(entityIds);

System.out.println("Found " + events.size() + " of 3 entities");
C
uint128_t entity_ids[3] = {entity_id1, entity_id2, entity_id3};

void on_batch_result(void* ctx, const geo_event_t* events, size_t count) {
    printf("Found %zu of 3 entities\n", count);
}

arch_get_latest_batch(client, entity_ids, 3, on_batch_result, NULL);

deleteEntities

Permanently delete all data for specified entities (GDPR compliance).

Request

Field Type Required Description
entity_ids u128[] Yes Entities to delete (1 to 10,000)

Response

Field Type Description
deleted_count u32 Number of entities actually deleted
not_found_count u32 Number of entities that didn’t exist

Errors

Code Name Description Retryable
101 INVALID_ENTITY_ID One or more entity IDs are zero No

curl Example

# Delete all data for specified entities (GDPR erasure)
curl -X DELETE http://localhost:3000/entities \
  -H "Content-Type: application/json" \
  -d '{
    "entity_ids": [
      "550e8400-e29b-41d4-a716-446655440000",
      "550e8400-e29b-41d4-a716-446655440001"
    ]
  }'

SDK Examples

Node.js
const result = await client.deleteEntities([entityId1, entityId2])

console.log(`Deleted: ${result.deleted_count}`)
console.log(`Not found: ${result.not_found_count}`)
Python
result = client.delete_entities([entity_id1, entity_id2])

print(f"Deleted: {result.deleted_count}")
print(f"Not found: {result.not_found_count}")
Go
result, err := client.DeleteEntities([]types.Uint128{entityID1, entityID2})
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Deleted: %d, Not found: %d\n", result.DeletedCount, result.NotFoundCount)
Java
DeleteResult result = client.deleteEntities(List.of(entityId1, entityId2));

System.out.println("Deleted: " + result.getDeletedCount());
System.out.println("Not found: " + result.getNotFoundCount());
C
uint128_t entity_ids[2] = {entity_id1, entity_id2};

delete_result_t result;
arch_delete_entities(client, entity_ids, 2, &result);

printf("Deleted: %u, Not found: %u\n", result.deleted_count, result.not_found_count);

Request/Response Formats

Batch Semantics

Batch operations (createBatch/commit) are atomic: either all events in the batch commit together, or none do. However, individual events within a batch can fail validation while others succeed.

Atomic commit:

  • All valid events are committed in a single transaction
  • Replication to quorum is guaranteed before response
  • On failure, no events from the batch are committed

Per-event results:

  • Each event in the batch gets an individual result code
  • A batch can “succeed” (commit) even if some events have validation errors
  • Check result.error for each event to detect partial failures

Pagination

All query operations use cursor-based pagination:

Field Type Direction Description
limit u32 Request Maximum events per page (default: 1,000, max: 10,000)
cursor bytes Both Opaque pagination token
has_more bool Response True if more results exist

Best practices:

  • Use a reasonable limit (1,000 is usually sufficient)
  • Don’t parse or modify the cursor - treat it as opaque
  • Cursors may expire if the underlying data changes significantly

Result Ordering

Query results are returned in deterministic order based on S2 cell ID. This ordering:

  • Ensures consistent pagination (no duplicates or gaps)
  • Groups spatially nearby entities together
  • Is not sorted by distance from query center

To sort by distance, sort results client-side after receiving them.

Error Handling

Error Categories

Errors are grouped into ranges by category:

Range Category General Handling
0 Success Operation completed
1-99 Protocol Check client version, message format
100-199 Validation Fix request parameters
200-299 State Check cluster health, retry if transient
300-399 Resource Reduce batch size, check limits
400-499 Security Check gateway/service authn/authz policy
500-599 Internal Open an issue with logs and reproduction details (should not occur)

Retry Semantics

SDKs automatically retry transient errors with exponential backoff. See SDK Retry Semantics for detailed configuration.

Retryable errors (automatically retried):

  • 211 - Cluster unavailable (no quorum)
  • 220 - Not shard leader
  • 222 - Resharding in progress
  • Network timeouts

Non-retryable errors (fail immediately):

  • 100-199 - Validation errors (fix the request)
  • 300-399 - Resource limits (reduce batch size)

For the complete error reference, see Error Codes.

Error Examples

This section shows example requests that trigger common errors and how to fix them.

InvalidLatitude (100)

Request that triggers error:

# Latitude 100 is out of range (valid: -90 to +90)
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "lat_nano": 100000000000,
      "lon_nano": -122419400000,
      "group_id": 1
    }]
  }'

Corrected request:

# Use valid latitude in nanodegrees (-90e9 to +90e9)
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }]
  }'

InvalidLongitude (100)

Request that triggers error:

# Longitude 200 is out of range (valid: -180 to +180)
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "lat_nano": 37774900000,
      "lon_nano": 200000000000,
      "group_id": 1
    }]
  }'

Corrected request:

# Use valid longitude in nanodegrees (-180e9 to +180e9)
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }]
  }'

BatchTooLarge (300)

Request that triggers error:

# Batch with more than 10,000 events
# (simplified - actual error occurs with >10,000 events array)
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [ /* 10,001+ events */ ]
  }'

Corrected approach:

# Split into multiple batches of 10,000 or fewer
# Batch 1
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{"events": [ /* first 10,000 events */ ]}'

# Batch 2
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{"events": [ /* remaining events */ ]}'

EntityNotFound (getLatest returns null)

Request:

# Query for non-existent entity
curl http://localhost:3000/entity/00000000-0000-0000-0000-000000000001

Response:

{
  "event": null,
  "found": false
}

Note: This is not an error - the API returns found: false for non-existent entities. Check the found field to handle this case.

Common Patterns

This section describes common usage patterns for the ArcherDB API.

Pagination

All query operations support cursor-based pagination for handling large result sets.

# First page
curl -X POST http://localhost:3000/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat": 37.7749,
    "center_lon": -122.4194,
    "radius_m": 10000,
    "limit": 1000
  }'

# Response includes cursor if has_more is true:
# {"events": [...], "has_more": true, "cursor": "abc123..."}

# Next page - use the cursor from previous response
curl -X POST http://localhost:3000/query/radius \
  -H "Content-Type: application/json" \
  -d '{
    "center_lat": 37.7749,
    "center_lon": -122.4194,
    "radius_m": 10000,
    "limit": 1000,
    "cursor": "abc123..."
  }'

Best practices:

  • Use limit of 1,000 for most cases (good balance of latency vs. round trips)
  • Treat cursors as opaque - don’t parse or modify them
  • Cursors may expire if underlying data changes significantly

Idempotent Upsert Pattern

Use upsert mode for safe retries and idempotent operations:

# Upsert mode - safe to retry, won't create duplicates
curl -X POST http://localhost:3000/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "entity_id": "550e8400-e29b-41d4-a716-446655440000",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }],
    "mode": "upsert"
  }'

# Running the same request again updates rather than fails

When to use insert vs. upsert:

  • insert: When you want to detect duplicate submissions (fails if exists)
  • upsert: For idempotent operations, retry safety, last-writer-wins semantics (recommended)

Batch Insert Optimization

Optimize throughput by batching events:

Batch Size Typical Latency Throughput Use Case
1-100 1-5 ms Low Real-time updates
100-1,000 5-20 ms Medium Periodic uploads
1,000-5,000 20-50 ms High Bulk imports
5,000-10,000 50-100 ms Maximum Initial data load

Recommended approach for bulk imports:

# Use batches of 1,000-5,000 events for optimal throughput
# Send multiple batches in parallel from multiple clients for maximum speed

# Client 1
curl -X POST http://localhost:3000/events -d '{"events": [/* batch 1 */], "mode": "upsert"}'

# Client 2 (in parallel)
curl -X POST http://localhost:3000/events -d '{"events": [/* batch 2 */], "mode": "upsert"}'

Error Retry Pattern

Handle transient errors with exponential backoff:

import time
import random

def insert_with_retry(client, events, max_retries=3):
    """Insert events with exponential backoff for transient errors."""
    for attempt in range(max_retries):
        try:
            return client.create_events(events, mode='upsert')
        except ArcherDBError as e:
            if not e.retryable:
                raise  # Non-retryable error, fail immediately

            if attempt == max_retries - 1:
                raise  # Last attempt, give up

            # Exponential backoff with jitter
            delay = (2 ** attempt) + random.uniform(0, 1)
            time.sleep(delay)

    raise RuntimeError("Should not reach here")

Retryable errors (safe to retry):

  • 211 - Cluster unavailable (no quorum)
  • 220 - Not shard leader
  • 222 - Resharding in progress
  • Network timeouts

Non-retryable errors (fix request first):

  • 100 - Invalid coordinates
  • 101 - Invalid entity ID
  • 300 - Batch too large

Rate Limits and Quotas

ArcherDB does not implement server-side rate limiting. Instead, limits are enforced through connection and batch constraints.

Connection Limits

Limit Default Description
Pool size 1 Connections per client (configurable via pool_size)
Max concurrent requests Pool size One request per connection

Increase pool_size for higher throughput. Each connection adds server-side overhead.

Batch Size Limits

Limit Value Description
Max events per batch 10,000 Insert/upsert batch size
Max entity IDs per lookup 10,000 getLatestBatch, deleteEntities
Max polygon vertices 1,000 queryPolygon outer ring
Max polygon holes 100 queryPolygon interior rings

Query Result Limits

Limit Default Max Description
Results per page 1,000 10,000 Events returned per query

Use pagination for larger result sets.

Performance Guidelines

Batch Size Typical Latency Throughput
1-100 1-5 ms Low
100-1,000 5-20 ms Medium
1,000-5,000 20-50 ms High
5,000-10,000 50-100 ms Maximum

For maximum throughput:

  • Use batches of 1,000-5,000 events
  • Use multiple client connections (pool_size)
  • Distribute load across client instances

Wire Protocol

For most use cases, use the official SDKs. This section provides protocol details for advanced users who need to implement custom clients.

Overview

ArcherDB uses a custom binary protocol over TCP:

  • Transport: TCP on trusted private networks (TLS termination external)
  • Framing: Length-prefixed messages
  • Encoding: Little-endian binary
  • Compression: None (data is already compact)

Message Format

Every message has the following structure:

+----------------+----------------+------------------+
| Length (4 B)   | Header (16 B)  | Payload (var)    |
+----------------+----------------+------------------+
Field Size Description
Length 4 bytes Total message size (excluding this field)
Header 16 bytes Message type, client ID, sequence number
Payload Variable Operation-specific data

Connection Establishment

  1. Client connects to any replica
  2. Client sends Register message with cluster ID
  3. Server responds with session ID
  4. Client uses session ID for all subsequent requests

Advanced: Source Reference

For full protocol details, see the source files:

  • Message definitions: src/message.zig
  • Protocol encoding: src/protocol.zig
  • Error codes: src/error_codes.zig

The SDKs implement this protocol correctly and handle edge cases like reconnection, request deduplication, and shard routing.

SDK Documentation

  • src/clients/python/README.md - Python client library
  • src/clients/node/README.md - Node.js/TypeScript client library
  • src/clients/go/README.md - Go client library
  • src/clients/java/README.md - Java client library
  • src/clients/c/README.md - C client library
Edit this page

Tier Profiles and Runtime Presets

This document defines the intended behavior of ArcherDB tier presets as open-source runtime and capacity profiles. It is normative guidance for release artifacts, performance tuning, and future configuration changes.

Canonical Tier Names

ArcherDB supports exactly these build tiers:

  • lite
  • standard
  • pro
  • enterprise
  • ultra

Tier Intent

Standard through ultra tiers share the same high-performance runtime profile and differ only in capacity. The lite tier uses a reduced-overhead runtime (fewer clients, smaller pipeline and journal) so its data file fits on a dev laptop.

Tier Primary intent RAM index (default) Storage (default / max) Runtime
lite Demo/evaluation — small footprint, fast startup 128 MiB 4 GiB / 4 GiB lite (64 clients, journal 256)
standard Baseline production capacity tier 4 GiB 64 GiB / 256 GiB high-perf (256 clients, journal 1024)
pro Mid-tier capacity profile 16 GiB 512 GiB / 2 TiB high-perf
enterprise Large-capacity production profile 32 GiB 4 TiB / 16 TiB high-perf
ultra Highest-capacity profile 64 GiB 16 TiB / 64 TiB high-perf

Non-Negotiables

  • Standard through ultra tiers must use the same high-performance runtime knobs (request envelope, LSM tuning, I/O concurrency, pipeline settings).
  • The lite tier may reduce runtime parameters (clients, pipeline, journal, compaction batch size, I/O concurrency) to shrink fixed overhead.
  • Tier progression must be monotonic by capacity from lite to ultra.
  • Capacity boundaries should be enforced through RAM and disk quotas, not transport/request ceilings.
  • Tier names should remain canonical; do not introduce compatibility aliases.

Release Artifact Guidance

  • Publish tier-labeled binaries if you ship prebuilt artifacts (for example: archerdb-lite, archerdb-standard, …).
  • Make lite the default recommendation for demos/evaluation.
  • Build release artifacts with performance-appropriate optimization settings for the release target.

Change Checklist (When Editing Tier Defaults)

  1. Confirm standard–ultra tiers still share the same runtime/performance knobs.
  2. Confirm lite uses the lite runtime; standard–ultra use the high-perf runtime.
  3. Confirm only RAM/disk quotas differ across tiers within each runtime class.
  4. Verify tier ordering remains monotonic by capacity (lite -> standard -> pro -> enterprise -> ultra).
  5. Update this document and linked user-facing docs in the same change.
Edit this page

ArcherDB Hardware Requirements

This document specifies minimum and recommended hardware requirements for ArcherDB deployments, along with cloud instance mappings and sizing formulas.

Table of Contents

Tier Objective Summary

Use hardware planning together with capacity quotas:

Tier Runtime posture Capacity posture
lite Same high-performance runtime as all tiers Smallest RAM/disk quota
standard Same high-performance runtime as all tiers Larger RAM/disk quota
pro Same high-performance runtime as all tiers Mid-tier RAM/disk quota
enterprise Same high-performance runtime as all tiers Large RAM/disk quota
ultra Same high-performance runtime as all tiers Highest RAM/disk quota

See Tier Profiles for canonical runtime/capacity intent.

Quick Reference

By Entity Count

Entities Min RAM Rec RAM Min Disk Rec Disk Min CPU
1M 2 GB 4 GB 10 GB 50 GB 2 cores
10M 4 GB 8 GB 50 GB 200 GB 4 cores
100M 16 GB 32 GB 200 GB 1 TB 8 cores
500M 64 GB 96 GB 1 TB 5 TB 16 cores
1B 96 GB 128 GB 2 TB 10 TB 32 cores

By Throughput Target

Events/sec Min RAM Rec RAM Rec CPU Rec Disk
10K 4 GB 8 GB 4 cores SATA SSD
100K 8 GB 16 GB 8 cores NVMe
500K 16 GB 32 GB 16 cores NVMe Gen3+
1M 32 GB 64 GB 32 cores NVMe Gen4

Minimum Requirements

These are the absolute minimum specifications for ArcherDB to function. Performance may be limited.

Development/Testing

For development, testing, and small-scale deployments (<1M entities):

Component Minimum Notes
CPU 2 cores, x86-64 Must support AES-NI
RAM 2 GB Limits entity count to ~20M
Disk 10 GB SSD HDD not supported
Disk Speed 100 MB/s seq SATA SSD sufficient
Network 100 Mbps For replication

Production (Single Node)

For production single-node deployments (<100M entities):

Component Minimum Notes
CPU 4 cores, x86-64 AVX2 recommended
RAM 8 GB Limits entity count to ~70M
Disk 100 GB NVMe Direct I/O requires NVMe
Disk Speed 1 GB/s seq NVMe Gen3 minimum
Network 1 Gbps 10 Gbps for replication

Production (Cluster)

For production cluster deployments:

Component Minimum per Node Notes
CPU 8 cores, x86-64 AVX2 highly recommended
RAM 16 GB All nodes must match
Disk 200 GB NVMe Replicas need identical storage
Disk Speed 2 GB/s seq NVMe Gen3+
Network 10 Gbps Between replicas

Standard Production

For typical production workloads (100M-500M entities, <500K events/sec):

Component Specification Rationale
CPU 16 cores, Intel Xeon or AMD EPYC Handles compaction + queries
RAM 64 GB ECC 500M entities + cache
Disk 1 TB NVMe Gen4 3+ GB/s for low latency
Network 25 Gbps Replication headroom

High Performance

For demanding workloads (>500M entities, >500K events/sec):

Component Specification Rationale
CPU 32+ cores, latest gen Max throughput
RAM 128-256 GB ECC Billions of entities
Disk 2+ TB NVMe Gen5 7+ GB/s peak
Network 100 Gbps Multi-region replication

CPU Requirements

Required CPU features:

Feature Required Why
x86-64 Yes Instruction set
AES-NI Yes Aegis-128L checksums and crypto primitives used by core internals
AVX2 Recommended SIMD operations
RDTSCP Recommended High-resolution timing

ARM64 (Apple Silicon, Graviton) is also supported.

Disk Requirements

Metric Minimum Recommended High Perf
Sequential Read 1 GB/s 3 GB/s 7 GB/s
Sequential Write 500 MB/s 1.5 GB/s 5 GB/s
Random Read IOPS 50K 200K 500K
Random Write IOPS 30K 100K 300K
Latency (p99) <1ms <0.5ms <0.2ms

Important: ArcherDB uses Direct I/O. Ensure:

  • File system supports O_DIRECT (ext4, xfs, btrfs)
  • No network-attached storage for data files
  • NVMe strongly preferred over SATA

Memory Sizing

Memory Formula

ArcherDB maintains a RAM index for O(1) entity lookups:

Index Memory = (Entity Count / Load Factor) x Entry Size

Where:
- Load Factor = 0.70 (target 70% utilization)
- Entry Size = 96 bytes per runtime slot (64-byte entry plus scan-helper arrays)

Recommended RAM = Index Memory x 1.4 (headroom for cache, buffers)

Memory Sizing Table

Entity Count Index Size Min RAM Rec RAM
1 Million 137 MB 2 GB 4 GB
10 Million 1.37 GB 4 GB 8 GB
50 Million 6.86 GB 12 GB 16 GB
100 Million 13.7 GB 24 GB 32 GB
250 Million 34.3 GB 48 GB 64 GB
500 Million 68.6 GB 96 GB 128 GB
1 Billion 137 GB 160 GB 192 GB

Memory Allocation Breakdown

For a production node with 128 GB RAM:

Component Allocation Purpose
Index ~69 GB Entity lookup and scan-helper arrays (500M entities)
Block Cache 16 GB LSM tree read caching
Query Buffers 2 GB Result assembly
VSR Buffers 2 GB Replication pipeline
OS/Overhead 16 GB Kernel, page cache

Storage Sizing

Storage Formula

Base Storage = Entity Count x Event Size x History Depth

Where:
- Event Size = 128 bytes per GeoEvent
- History Depth = average events per entity

With Safety Margin:
Recommended Storage = Base Storage x 1.5

Storage Sizing Table

Entities Latest Only 10x History 50x History
1M 128 MB 1.3 GB 6.4 GB
10M 1.3 GB 13 GB 64 GB
100M 13 GB 130 GB 640 GB
500M 64 GB 640 GB 3.2 TB
1B 128 GB 1.3 TB 6.4 TB

Storage Type Guidelines

Workload Storage Type Why
Development SATA SSD Cost-effective
Standard Production NVMe Gen3 Good balance
High Throughput NVMe Gen4/Gen5 Maximum IOPS
High Capacity NVMe + S3 tiering Cost-effective scale

Network Requirements

Bandwidth Sizing

Replication Bandwidth = Events/sec x Event Size x Replicas

Example: 100K events/sec with 3 replicas
= 100,000 x 128 x 2 (primary to backups)
= 25.6 MB/s = 205 Mbps

Network Requirements by Scale

Events/sec Min Bandwidth Rec Bandwidth
10K 100 Mbps 1 Gbps
100K 1 Gbps 10 Gbps
500K 5 Gbps 25 Gbps
1M 10 Gbps 50 Gbps

Latency Requirements

Scenario Max Latency Recommended
Same rack 1ms <0.1ms
Same datacenter 5ms <1ms
Same region 20ms <5ms
Cross-region 100ms <50ms

Cloud Instance Mapping

AWS EC2

Use Case Instance vCPUs RAM Storage Cost/mo
Dev/Test t3.large 2 8 GB gp3 ~$60
Small Prod m6i.xlarge 4 16 GB gp3 ~$140
Standard Prod m6i.4xlarge 16 64 GB io2 ~$560
High Perf m6i.8xlarge 32 128 GB io2 ~$1,120
Extreme i4i.4xlarge 16 128 GB local NVMe ~$1,000

Storage Notes:

  • Use io2 for production (up to 64K IOPS)
  • i4i instances have local NVMe (best performance)
  • gp3 sufficient for dev/test

Google Cloud

Use Case Instance vCPUs RAM Storage
Dev/Test e2-standard-2 2 8 GB pd-ssd
Small Prod n2-standard-4 4 16 GB pd-ssd
Standard Prod n2-standard-16 16 64 GB pd-extreme
High Perf n2-standard-32 32 128 GB local SSD
Extreme c3-standard-44 44 176 GB local SSD

Azure

Use Case Instance vCPUs RAM Storage
Dev/Test Standard_D2s_v5 2 8 GB Premium SSD
Small Prod Standard_D4s_v5 4 16 GB Premium SSD
Standard Prod Standard_D16s_v5 16 64 GB Premium SSD v2
High Perf Standard_D32s_v5 32 128 GB Ultra Disk
Extreme Standard_L16s_v3 16 128 GB local NVMe

Bare Metal Providers

Provider Configuration Approx Cost
Hetzner AX102 32 cores, 128 GB, 2x NVMe ~$180/mo
OVH Advance-2 16 cores, 64 GB, 2x NVMe ~$120/mo
Vultr Bare Metal 24 cores, 256 GB, NVMe ~$350/mo
Equinix m3.large 24 cores, 64 GB, NVMe ~$500/mo

Sizing Calculator

Quick Sizing Formula

Required RAM (GB) = ceil(entities / 10_000_000) * 1.5 + 4

Required Disk (GB) = (entities * 128 * history_depth) / (1024^3) * 1.5

Required Cores = max(4, ceil(events_per_sec / 100_000) * 4)

Example: 100M Entities, 50K events/sec

RAM:
  Index = (100M / 0.7) * 96 = 13.7 GB
  Recommended = 13.7 * 1.4 = 19.2 GB
  With headroom = 24 GB minimum, 32 GB recommended

Disk (with 20x history):
  Base = 100M * 128 * 20 = 256 GB
  Recommended = 256 * 1.5 = 384 GB
  Actual = 512 GB NVMe

CPU:
  Base = 50K / 100K * 4 = 2 cores
  Recommended = 8 cores (for compaction headroom)

Network:
  Replication = 50K * 128 * 2 = 12.8 MB/s
  Recommended = 1 Gbps

Sizing Worksheet

1. Entity Estimation
   Current entities:     ____________
   Annual growth rate:   ____________%
   Planning horizon:     ____________ years
   Projected entities:   ____________

2. Throughput Estimation
   Peak events/sec:      ____________
   Average events/sec:   ____________
   Batch size:           ____________

3. Memory Calculation
   Index memory:         ____________ GB  (entities / 0.7 * 96 / 1GB)
   With headroom:        ____________ GB  (index * 1.4)
   Actual RAM:           ____________ GB  (round up to available)

4. Storage Calculation
   Base storage:         ____________ GB  (entities * 128 / 1GB)
   With history:         ____________ GB  (base * history_depth)
   With margin:          ____________ GB  (with_history * 1.5)
   Actual disk:          ____________ GB  (round up)

5. Instance Selection
   Cloud provider:       ____________
   Instance type:        ____________
   Monthly cost:         $____________
Edit this page

LSM Tree Tuning Guide

This guide describes ArcherDB’s LSM tuning model after the tier redesign.

All tier presets now share one high-performance runtime profile. Tier differences exist only in capacity quotas (RAM index and disk limits).

Runtime Model

ArcherDB uses one shared runtime profile for lite, standard, pro, enterprise, and ultra.

Runtime parameter Shared value Why
message_size_max 10 MiB Prevents request-size bottlenecks in ingest/query paths
block_size 1 MiB Maximizes sequential I/O throughput
lsm_levels 8 Large capacity envelope with predictable compaction behavior
lsm_growth_factor 8 Balanced write/read amplification
lsm_compaction_ops 128 Larger memtables, fewer flush cycles
lsm_manifest_compact_extra_blocks 3 Keeps manifest growth bounded
lsm_table_coalescing_threshold_percent 35 Aggressive coalescing for space efficiency
pipeline_prepare_queue_max 24 Higher parallelism in the prepare pipeline
journal_slot_count 1024 Keeps WAL sizing practical while preserving throughput
journal_iops_write_max 32 Matches journal safety invariants with current WAL layout
journal_iops_read_max 24 High read-side WAL concurrency
grid_iops_read_max 96 High read concurrency for LSM/grid access
grid_iops_write_max 96 High write concurrency for compaction/replication work

Capacity-Only Tier Matrix

These are the only intended differences between tier profiles:

Tier RAM index default Storage default / max
lite 128 MiB 4 GiB / 4 GiB
standard 4 GiB 64 GiB / 256 GiB
pro 16 GiB 512 GiB / 2 TiB
enterprise 32 GiB 4 TiB / 16 TiB
ultra 64 GiB 16 TiB / 64 TiB

What “Capacity-Only” Means in Practice

  • If ingest stops because of TOO_MUCH_DATA (status=1) before RAM or disk is exhausted, that is a transport/request-shape issue, not a tier capacity limit.
  • Correct capacity failures should present as resource boundaries, for example:
    • RAM index pressure (IndexDegraded)
    • Storage size limit exhaustion
  • Tier selection should not be used to tune throughput or latency behavior.

Hardware Guidance

Runtime tuning is shared, so hardware determines absolute throughput:

Target throughput Suggested CPU Suggested RAM Suggested storage
100K events/sec 8 cores 16+ GB NVMe Gen3+
500K events/sec 16 cores 32+ GB Fast NVMe
1M+ events/sec 32+ cores 64+ GB NVMe Gen4/Gen5

Use tier quotas for capacity governance, not performance throttling.

Benchmarking

Quick local smoke

zig-out/bin/archerdb benchmark --event-count=100000 --query-uuid-count=10000 --query-radius-count=1000 --query-polygon-count=100

Maintained benchmark harness

python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60

Capacity test (real run)

python3 scripts/test_capacity_limits.py --config lite --optimize ReleaseFast
python3 scripts/test_capacity_limits.py --config standard --optimize ReleaseFast

By default, capacity test artifacts are written to /tmp/archerdb_capacity_runs.

Interpreting Results

For capacity runs, verify:

  1. No early transport bottleneck (status=1) at normal batch sizes.
  2. Throughput remains in the same order of magnitude across tiers on the same hardware.
  3. Failure reason changes with capacity quotas, not tier runtime behavior.

Example summary fields:

  • events_inserted
  • unique_entries
  • cpu_percent_avg / cpu_percent_peak
  • ram_rss_avg_bytes / ram_rss_peak_bytes
  • disk_logical_bytes / disk_physical_bytes_from_du
  • failure_reason and first_error_code

Troubleshooting

Capacity run fails with status=1

  • Reduce per-request payload in the test runner (adaptive batch backoff should already do this).
  • Ensure the test uses the intended tier build and optimize mode.
  • Confirm server request envelope has not been overridden to a small value.

Capacity run fails too early on RAM index

  • This is expected for lower-capacity tiers.
  • --ram-index-size can lower RAM index budget at runtime but cannot exceed the tier cap.
  • Increase ram_index_size_default only if product intent requires a higher capacity boundary.

Capacity run fails on storage limit

  • Verify storage_size_limit_default and storage_size_limit_max for the selected tier.
  • Use larger tier quotas when the workload requires longer retention.

References

  • src/config.zig
  • src/constants.zig
  • scripts/test_capacity_limits.py
  • zig-out/bin/archerdb benchmark
  • test_infrastructure/benchmarks/cli.py
Edit this page

Journal Sizing for ArcherDB

This document calculates requirements for ArcherDB’s target throughput of 1M ops/sec.

Current Configuration

Parameter Value Notes
journal_slot_count 1,024 Maximum batch entries in journal
message_size_max 1 MiB Maximum message/prepare size
vsr_checkpoint_ops 960 Checkpoint interval
Header size 256 bytes Per message header
GeoEvent size 128 bytes Per record

Journal Storage Layout

Journal = Headers Zone + Prepares Zone
       = (1,024 × 256 bytes) + (1,024 × 1 MiB)
       = 256 KiB + 1 GiB
       ≈ 1 GiB total

Retention Time Formula

Retention = journal_slot_count / ops_per_second

At 1M ops/sec with Current Settings

1,024 slots / 1,000,000 ops/sec = 1.024 milliseconds

This means if a replica crashes, it has approximately 1ms of operations in the journal before wrap.

GeoEvent Capacity per Message

Max events/message = (message_size_max - header_size) / geo_event_size
                   = (1,048,576 - 256) / 128
                   = 8,190 events

Validation: Is 8192 Slots Sufficient?

Per the spec, we need to validate journal_slot_count=8192 for 1M ops/sec:

Retention at 8,192 slots:
  8,192 / 1,000,000 = 8.192 milliseconds

With 8,190 events per message:
  Throughput = 8,190 × (1,000,000 / 8,192) ≈ 1B events/sec

Assessment: 8,192 slots provides adequate retention (~8ms) for ArcherDB’s target. The checkpoint interval would need adjustment:

vsr_checkpoint_ops = journal_slot_count - (pipeline_prepare_queue_max × 2) - lsm_compaction_ops
                   = 8,192 - 16 - 32
                   = 8,144 ops per checkpoint

Durability Constraints

The following invariant must hold (from constants.zig):

assert(vsr_checkpoint_ops + lsm_compaction_ops +
       pipeline_prepare_queue_max * 2 <= journal_slot_count);

With 8,192 slots: 8,144 + 32 + 16 = 8,192

Recommendation for ArcherDB

For initial 1M ops/sec target, we recommend:

Parameter Value Rationale
journal_slot_count 8,192 8× retention for better recovery
message_size_max 1 MiB Unchanged, proven
vsr_checkpoint_ops 8,144 Max before wrap

Why Not Larger?

  • 8,192 slots = 8 GiB journal (reasonable for modern NVMe)
  • Retention of 8ms is adequate for replica recovery
  • Larger journals increase memory pressure and startup time
  • GeoEvents are 128 bytes

Scaling Beyond 1M ops/sec

For 10M ops/sec (future):

  • Consider journal_slot_count = 16,384 (16ms retention)
  • Or increase message_size_max to 4 MiB (32K events per batch)
  • Balance: retention time vs. batch latency vs. memory

Key Files

  • src/constants.zig: Derived configuration values
  • src/config.zig: Base configuration
  • src/vsr/journal.zig: WAL implementation
  • src/vsr.zig: Checkpoint logic

References

  • Source: src/vsr.zig
Edit this page

SDK Overview

ArcherDB ships five client SDKs over a shared geospatial operation surface:

SDK Package Primary Docs
Python archerdb src/clients/python/README.md
Node.js archerdb-node src/clients/node/README.md
Go github.com/ArcherDB-io/archerdb/src/clients/go src/clients/go/README.md
Java com.archerdb:archerdb-java (local install until Central publish) src/clients/java/README.md
C archerdb-c src/clients/c/README.md

Shared Capability Surface

The repo’s parity runner validates the same 14-operation surface across all five SDKs:

  • insert
  • upsert
  • delete
  • query-uuid
  • query-uuid-batch
  • query-radius
  • query-polygon
  • query-latest
  • ping
  • status
  • topology
  • ttl-set
  • ttl-extend
  • ttl-clear

See Parity Matrix for the current checked-in evidence summary.

Choosing An SDK

  • Use Python for scripting, analysis, and operational tooling.
  • Use Node.js for web backends and TypeScript-heavy applications.
  • Use Go for single-binary services and low-overhead deployments.
  • Use Java for JVM services and enterprise application stacks.
  • Use C when you need the lowest-level interface and manual control over callbacks, buffers, and threading.

Source Of Truth

The language-specific READMEs under src/clients/ remain the primary API and usage references. This directory provides the aggregate index and parity-oriented comparison material the top-level docs link to.

Edit this page

SDK Platform Support

What each SDK actually ships and what is validated, as of 2026-06-12. “Shipped” means a prebuilt native client library is included in the package/tree; “validated” means a CI lane or recorded live run exercises it.

Platform C Go Java Node.js Python
x86_64 Linux (glibc) shipped + validated shipped + validated shipped + validated shipped + validated shipped + validated
x86_64 Linux (musl) shipped + validated (Alpine CI lane) — (glibc lib only) shipped shipped shipped
aarch64 Linux (glibc/musl) shipped shipped (glibc) shipped shipped shipped
x86_64 macOS shipped + validated (macOS CI lane) shipped shipped shipped shipped
aarch64 macOS shipped + validated (macOS ARM64 CI lane) shipped shipped shipped shipped
x86_64 Windows not shipped not shipped not shipped not shipped not shipped

Notes:

  • aarch64 Linux artifacts are cross-compiled by zig build clients:* but no CI lane runs on aarch64 hosts; treat as best-effort until one exists.
  • Windows: no SDK ships Windows binaries. The C tree previously contained an x86_64-windows/ directory holding only tb_client.dll/tb_client.lib — misnamed TigerBeetle-era leftovers that nothing could link as libarch_client (removed 2026-06-12; zig build clients:c never refreshed that directory because Windows is not in its target list). The Python package’s Windows trove classifier was removed for the same honesty reason. Adding Windows support to any SDK requires, in this order: a windows target in the relevant zig build clients:* step, the prebuilt committed/shipped, and a Windows job in CI exercising the live suite. Do not re-add platform claims ahead of that lane.
  • The platform a wheel/jar/npm package selects at runtime is detected at import time (libc detection on Linux); unsupported platforms fail with an explicit “Unsupported platform” error rather than undefined behavior.
Edit this page

SDK Comparison Matrix

This matrix summarizes the common ArcherDB operation surface that is intended to behave consistently across all five SDKs.

Core Operation Matrix

Feature Python Node.js Go Java C
Insert events yes yes yes yes yes
Upsert events yes yes yes yes yes
Delete entities yes yes yes yes yes
Query by UUID yes yes yes yes yes
Batch query by UUID yes yes yes yes yes
Radius query yes yes yes yes yes
Polygon query yes yes yes yes yes
Latest query yes yes yes yes yes
Ping yes yes yes yes yes
Status yes yes yes yes yes
Topology discovery yes yes yes yes yes
TTL set yes yes yes yes yes
TTL extend yes yes yes yes yes
TTL clear yes yes yes yes yes

Packaging And Ergonomics

Concern Python Node.js Go Java C
Generated README yes yes yes yes no
Native binding layer yes yes yes yes direct C ABI
128-bit IDs ergonomic type Python int BigInt custom Uint128 wrapper types raw struct / integer API
Thread-safe client abstraction yes yes yes yes manual coordination required
Lowest-level manual control medium low medium medium high

Validation Notes

  • Repo parity evidence is summarized in docs/PARITY.md.
  • SDK-specific setup, examples, and type details live in the language READMEs under src/clients/.
  • The C SDK is intentionally the lowest-level interface and exposes callback- and buffer-oriented behavior directly. See SDK Limitations.
Edit this page

SDK Comparison Matrix

This matrix summarizes the common ArcherDB operation surface that is intended to behave consistently across all five SDKs.

Core Operation Matrix

Feature Python Node.js Go Java C
Insert events yes yes yes yes yes
Upsert events yes yes yes yes yes
Delete entities yes yes yes yes yes
Query by UUID yes yes yes yes yes
Batch query by UUID yes yes yes yes yes
Radius query yes yes yes yes yes
Polygon query yes yes yes yes yes
Latest query yes yes yes yes yes
Ping yes yes yes yes yes
Status yes yes yes yes yes
Topology discovery yes yes yes yes yes
TTL set yes yes yes yes yes
TTL extend yes yes yes yes yes
TTL clear yes yes yes yes yes

Packaging And Ergonomics

Concern Python Node.js Go Java C
Generated README yes yes yes yes no
Native binding layer yes yes yes yes direct C ABI
128-bit IDs ergonomic type Python int BigInt custom Uint128 wrapper types raw struct / integer API
Thread-safe client abstraction yes yes yes yes manual coordination required
Lowest-level manual control medium low medium medium high

Validation Notes

  • Repo parity evidence is summarized in docs/PARITY.md.
  • SDK-specific setup, examples, and type details live in the language READMEs under src/clients/.
  • The C SDK is intentionally the lowest-level interface and exposes callback- and buffer-oriented behavior directly. See SDK Limitations.
Edit this page

SDK Limitations

This document captures practical SDK constraints that matter when shipping ArcherDB applications.

Shared Limits

  • The SDKs expose the database operation surface; they do not add built-in authn/authz, TLS, or backup orchestration. Those controls are part of ArcherDB’s infrastructure-managed deployment model.
  • Checked-in parity evidence is a snapshot, not a perpetual guarantee. Re-run parity before release or when changing protocol behavior.
  • The SDKs are only as capable as the underlying server surface. Features that are still experimental or operator-only should not be marketed as fully productized SDK workflows.

Language-Specific Constraints

Python

  • Best suited for application logic, scripting, and operational tooling, not raw highest-throughput client hot paths.
  • Build and test flows assume a working Python toolchain and the repo’s native extension build path.

Node.js

  • Uses BigInt for 64-bit and 128-bit values. Callers must preserve BigInt semantics end-to-end.
  • Build and test flows depend on native bindings and a working Node.js toolchain.

Go

  • Uses ArcherDB-specific types such as Uint128 and geospatial helper builders instead of native 128-bit language primitives.
  • Build and test flows depend on generated/native client artifacts plus a working Go toolchain.

Java

  • Uses JNI/native bindings and requires a working JVM plus native artifact generation in build/test flows.
  • JVM ergonomics differ from the other SDKs; release validation must include the JNI path, not only pure-Java compilation.
  • The Java multi-region client has latency-aware NEAREST routing: a background LatencyProber periodically TCP-connects to each configured region, maintains rolling RTT averages per region, and selection picks the healthy region with the lowest average. FOLLOWER routing remains deterministic (first follower). Before the first probe completes NEAREST falls back to config-order, so the very first request never stalls on a probe.

Java And Node Unit Tests

  • Checked-in Java and Node unit tests include API-shape and wire-format coverage that runs without a live cluster. Those tests are useful, but they are not a substitute for cluster-backed SDK integration evidence.

C

  • The C SDK is the lowest-level API and intentionally exposes callback-based completion, manual buffer handling, and explicit lifecycle management.
  • The C client API is not the ergonomic default for multi-threaded applications; callers are responsible for respecting the thread-safety and pinned-memory constraints documented in arch_client.h.

Operational Caveat

Deep validation tiers and long-running benchmark publication are currently manual-dispatch workflows rather than scheduled GitHub Actions runs. SDK release confidence should therefore come from the checked workflow results and local/manual release-candidate runs, not from an assumption of continuous nightly coverage.

Edit this page

SDK Parity Matrix

Cross-SDK parity verification for ArcherDB. All 5 SDKs must produce identical results for identical operations.

Generated: 2026-06-12T03:27:03.818350Z

Summary

  • Total tests: 79
  • Passed: 79
  • Failed: 0

Methodology

Per Phase 14 CONTEXT.md decisions:

  • Verification strategy: Layered approach
    1. Direct comparison between all SDKs
    2. Python SDK as golden reference for tie-breaking
    3. Server responses as ultimate truth
  • Equality definition: Exact match required
    • Structural equality (same fields, types, values)
    • Exact byte equality for coordinates (nanodegrees, no epsilon tolerance)

Matrix (14 ops x 5 SDKs = 70 cells)

Operation Python Node.js Go Java C
delete PASS PASS PASS PASS PASS
insert PASS PASS PASS PASS PASS
ping PASS PASS PASS PASS PASS
query-latest PASS PASS PASS PASS PASS
query-polygon PASS PASS PASS PASS PASS
query-radius PASS PASS PASS PASS PASS
query-uuid PASS PASS PASS PASS PASS
query-uuid-batch PASS PASS PASS PASS PASS
status PASS PASS PASS PASS PASS
topology PASS PASS PASS PASS PASS
ttl-clear PASS PASS PASS PASS PASS
ttl-extend PASS PASS PASS PASS PASS
ttl-set PASS PASS PASS PASS PASS
upsert PASS PASS PASS PASS PASS

Legend: PASS = identical results, FAIL = mismatch, - = not tested

Edge Cases Verified

Per CONTEXT.md, all geographic edge cases are high priority:

Polar Regions

  • North pole (lat=90, any longitude)
  • South pole (lat=-90, any longitude)
  • Longitude ambiguity at poles

Antimeridian (Date Line)

  • lon=180 and lon=-180 (same line)
  • Queries spanning date line
  • Points near antimeridian

Zero Crossings

  • Equator (lat=0)
  • Prime meridian (lon=0)
  • Intersection (0, 0)

Running Parity Tests

# Start server
./zig/zig build run -- --config=lite

# Run all parity tests
python tests/parity_tests/parity_runner.py

# Run specific operation
python tests/parity_tests/parity_runner.py --ops insert query-radius

# Run specific SDKs
python tests/parity_tests/parity_runner.py --sdks python node go

CI Integration

Machine-readable report: reports/parity.json


Last updated: 2026-06-12T03:27:03.820843Z

Edit this page

ArcherDB SDK Verification Report

Date: April 9, 2026 Commit: 7b8908e0 Status: Current commit parity is green across all five SDKs

Executive Summary

This report replaces the stale January 31, 2026 SDK report.

The authoritative current SDK evidence is now the checked-in parity suite:

Current result:

  • 79/79 passed, 0 failed
  • base parity target: 70 cells (14 operations x 5 SDKs)
  • additional topology and failover topology cases also passed on the current commit

SDK Status

SDK Current Status Evidence
Python PASS parity green; focused filter cases rerun live
Node.js PASS parity green; focused filter cases rerun live
Go PASS parity green on refreshed current-source native artifacts
Java PASS parity green on refreshed current-source artifacts and fixed topology parsing
C PASS parity green with real topology response decoding

What Changed Since The Old Report

  • Go and Java are no longer in a blanket failure state.
  • The old report was based on a stale sample-driven pass from January 31, 2026.
  • The current source tree has:
    • refreshed packaged SDK artifacts before parity
    • fixed topology parsing in Java and C parity paths
    • removed stale test skips in Python and Node
    • relabeled skeleton-mode unit tests honestly so they are not mistaken for release evidence

Current Verification Sources

Cross-SDK Parity

  • full parity sweep:
    • python3 -u tests/parity_tests/parity_runner.py --start-cluster --cluster-port 4000 --cluster-nodes 1 -v --output reports/parity.json --markdown docs/PARITY.md
    • result: 79/79 passed, 0 failed

Focused Live SDK Checks

  • Python:
    • previously skipped filter paths now rerun live and green
  • Node:
    • cd tests/sdk_tests/node && npx tsc --noEmit
    • targeted Jest rerun for group/timestamp cases passed on a live node

Topology Verification

  • topology cases passed across Python, Node, Go, Java, and C for:
    • steady-state topology
    • leader failover topology
    • unhealthy-node topology

Interpretation

For the current GA SDK surface, the checked-in repo evidence no longer supports the older “Python/Node good, Go/Java broken” conclusion. The current truthful statement is:

  • all five SDKs pass the current parity suite
  • the parity artifacts in this repo are current as of April 9, 2026
  • remaining non-GA surfaces are documented as non-GA instead of being counted as SDK failures

Remaining Release-Evidence Work

The remaining SDK-adjacent release work is not functional parity failure. It is release proofing:

  • credentialed package-publish rehearsal for Java Central using the post-publish checksum verifier
  • broader release bundle refresh alongside current benchmark artifacts
Edit this page

Operations Runbook

This runbook provides operational procedures for running ArcherDB in production.

Table of Contents

Cluster Management

Starting a Cluster

Single Node (Development)

# Format data file
./archerdb format --cluster=0 --replica=0 --replica-count=1 /data/archerdb.db

# Start server
./archerdb start --addresses=3000 /data/archerdb.db

Production Cluster (3 Nodes)

Start replicas in any order - they will discover each other and elect a primary.

# All nodes use the same addresses list
ADDRESSES="node1:3000,node2:3000,node3:3000"

# Node 1 (replica 0)
./archerdb start --addresses=$ADDRESSES /data/archerdb.db

# Node 2 (replica 1)
./archerdb start --addresses=$ADDRESSES /data/archerdb.db

# Node 3 (replica 2)
./archerdb start --addresses=$ADDRESSES /data/archerdb.db

Stopping a Cluster

Graceful Shutdown:

# Send SIGTERM to each replica
kill -TERM $(pidof archerdb)

# Or use systemd
systemctl stop archerdb

Order doesn’t matter - replicas handle shutdown gracefully and will sync on restart.

Checking Cluster Status

# Check if process is running
systemctl status archerdb

# Check cluster health via client
./archerdb client ping --addresses=node1:3000,node2:3000,node3:3000

Cluster Membership Boundary

Use cluster status to inspect the current static membership:

# Show membership status
./archerdb cluster status --addresses=node1:3000,node2:3000,node3:3000 --cluster=12345

Notes:

  • The public archerdb cluster CLI currently supports status only; membership mutation remains an external orchestration concern.
  • Cluster membership is fixed by the startup --addresses set.
  • To change the node set, provision a replacement cluster with the desired addresses and perform a controlled migration or restore/cutover.

Coordinator Mode (Multi-Shard Routing)

Run the coordinator when clients need a single endpoint for multi-shard queries:

# Start with explicit shard list
./archerdb coordinator start \
  --bind=0.0.0.0:5000 \
  --shards=10.0.0.1:3000,10.0.0.2:3000

# Or start with topology discovery
./archerdb coordinator start --seed-nodes=10.0.0.1:3000 --bind=0.0.0.0:5000

# Check status / stop
./archerdb coordinator status --address=127.0.0.1:5000 --format=json
./archerdb coordinator stop --address=127.0.0.1:5000 --timeout=60

Monitoring

Key Metrics

Throughput Metrics

Metric Description Target
archerdb_operations_total{op="insert"} Insert operations/sec < 10,000/s per replica
archerdb_operations_total{op="query"} Query operations/sec Application-dependent
archerdb_batch_size Events per batch 500-5000 optimal

Latency Metrics

Metric Description Target
archerdb_request_duration_seconds{quantile="0.99"} P99 latency < 50ms
archerdb_request_duration_seconds{quantile="0.5"} P50 latency < 5ms
archerdb_replication_lag_ms Follower lag < 100ms

Resource Metrics

Metric Description Target
archerdb_disk_usage_bytes Data file size < 80% of disk
archerdb_memory_usage_bytes Memory usage < 80% of RAM
archerdb_connections_active Active clients < pool_size × clients

Consensus Metrics

Metric Description Alert Threshold
archerdb_view_changes_total Leader elections > 0 in 5min
archerdb_primary_changes_total Primary switches > 1/hour
archerdb_commit_latency_ms Consensus latency > 100ms

Prometheus Configuration

scrape_configs:
  - job_name: 'archerdb'
    static_configs:
      - targets:
        - node1:9090
        - node2:9090
        - node3:9090
    metrics_path: /metrics
    scrape_interval: 15s

Grafana Dashboards

Import the recommended dashboards:

  • Cluster Overview: Throughput, latency, resource usage
  • Replication Status: View changes, commit latency, follower lag
  • Client Metrics: Connection counts, retry rates, errors

Alerting

Critical Alerts

# Cluster Unavailable
- alert: ArcherDBClusterDown
  expr: sum(up{job="archerdb"}) < 2
  for: 30s
  labels:
    severity: critical
  annotations:
    summary: "ArcherDB cluster has lost quorum"
    description: "Less than 2 of 3 replicas are healthy"

# Disk Space Critical
- alert: ArcherDBDiskCritical
  expr: archerdb_disk_usage_bytes / archerdb_disk_total_bytes > 0.9
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "ArcherDB disk usage > 90%"

# High Latency
- alert: ArcherDBHighLatency
  expr: histogram_quantile(0.99, archerdb_request_duration_seconds) > 0.1
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "ArcherDB P99 latency > 100ms"

Warning Alerts

# Frequent View Changes
- alert: ArcherDBViewChanges
  expr: increase(archerdb_view_changes_total[5m]) > 3
  labels:
    severity: warning
  annotations:
    summary: "Frequent leader elections detected"

# Client Retry Rate High
- alert: ArcherDBHighRetryRate
  expr: rate(archerdb_client_retries_total[5m]) > 10
  labels:
    severity: warning
  annotations:
    summary: "High client retry rate"

# Replication Lag
- alert: ArcherDBReplicationLag
  expr: archerdb_replication_lag_ms > 1000
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "Follower replication lag > 1s"

Alert Response Guides

When Prometheus alerts fire, use these runbooks for investigation and resolution. Each runbook provides:

  • Quick reference (severity, metric, threshold)
  • Immediate action checklists
  • Investigation steps with diagnostic commands
  • Resolution procedures for common causes
  • Prevention guidelines

Alert to Runbook Mapping

Alert Severity Runbook
ArcherDBReplicaDown critical Replica Down
ArcherDBViewChangeFrequent warning View Changes
ArcherDBIndexDegraded critical Index Degraded
ArcherDBReadLatencyP99Warning warning High Read Latency
ArcherDBReadLatencyP99Critical critical High Read Latency
ArcherDBWriteLatencyP99Warning warning High Write Latency
ArcherDBWriteLatencyP99Critical critical High Write Latency
ArcherDBHighLatency warning High Write Latency
ArcherDBDiskSpaceWarning warning Disk Capacity
ArcherDBDiskSpaceCritical critical Disk Capacity
ArcherDBCompactionBacklog warning Compaction Backlog
ArcherDBDiskFillPrediction24h warning Disk Capacity
ArcherDBDiskFillPrediction6h critical Disk Capacity

Alert Severity Response Times

Severity Response Time Example Alerts
critical 15 minutes ReplicaDown, DiskSpaceCritical, IndexDegraded
warning 1 hour ViewChangeFrequent, LatencyWarning, CompactionBacklog

Quick Triage

When an alert fires:

  1. Check alert severity - Critical alerts need immediate attention
  2. Open the runbook - Click the runbook_url in the alert annotation
  3. Follow Immediate Actions - Complete the checklist in order
  4. Investigate - Use diagnostic commands to identify root cause
  5. Resolve - Follow resolution steps for the identified cause
  6. Document - Record what happened and any changes made

Scaling

Vertical Scaling

ArcherDB benefits from:

  • More RAM: Larger block cache, more in-flight requests
  • Faster SSD: NVMe recommended for production
  • More CPU cores: Better concurrent request handling

Sharding Strategy Selection

Choose a sharding strategy at cluster initialization:

./archerdb format --cluster=12345 --replica=0 --replica-count=3 \
  --sharding-strategy=jump_hash /data/archerdb.db

Trade-offs:

  • jump_hash (default): Uniform distribution, minimal movement on reshard, no extra memory.
  • virtual_ring: Supports weighted shards and uneven capacity, but adds lookup overhead and memory.
  • modulo: Legacy, power-of-2 only, ~50% movement on reshard.
  • spatial: Optimizes radius/polygon fan-out but adds two-hop entity lookups; best when spatial queries dominate.

Use ./archerdb info /data/archerdb.db to verify the configured strategy.

Horizontal Scaling (Read Replicas)

For read-heavy workloads, add read replicas:

# Add a read-only replica (replica 3 in a 3-node write cluster)
./archerdb format --cluster=12345 --replica=3 --replica-count=5 /data/archerdb.db
./archerdb start --addresses=$ADDRESSES --read-only /data/archerdb.db

Note: Read replicas don’t participate in consensus and may have slight lag.

Client-Side Scaling

  • Connection pooling: Use pool_size > 1 for high-throughput clients
  • Batch sizing: Larger batches (1000-5000) improve throughput
  • Multiple clients: Distribute load across client instances

Kubernetes Deployment

Deploy ArcherDB on Kubernetes using StatefulSets for stable network identities and persistent storage.

Prerequisites

  • Kubernetes 1.24 or later
  • kubectl configured with cluster access
  • A StorageClass supporting ReadWriteOnce volumes (e.g., gp3 on AWS, pd-ssd on GCP)
  • Network policy allowing inter-pod communication on port 3000

StatefulSet Deployment

Create a namespace and deploy the ArcherDB cluster:

kubectl create namespace archerdb

ConfigMap for cluster addresses:

# archerdb-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: archerdb-config
  namespace: archerdb
data:
  ADDRESSES: "archerdb-0.archerdb-headless.archerdb.svc.cluster.local:3000,archerdb-1.archerdb-headless.archerdb.svc.cluster.local:3000,archerdb-2.archerdb-headless.archerdb.svc.cluster.local:3000"

Headless Service for stable DNS:

# archerdb-headless.yaml
apiVersion: v1
kind: Service
metadata:
  name: archerdb-headless
  namespace: archerdb
  labels:
    app: archerdb
spec:
  ports:
    - port: 3000
      name: archerdb
    - port: 9090
      name: metrics
  clusterIP: None
  selector:
    app: archerdb

StatefulSet with 3 replicas:

# archerdb-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: archerdb
  namespace: archerdb
spec:
  serviceName: archerdb-headless
  replicas: 3
  selector:
    matchLabels:
      app: archerdb
  template:
    metadata:
      labels:
        app: archerdb
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: archerdb
                topologyKey: kubernetes.io/hostname
      containers:
        - name: archerdb
          image: archerdb/archerdb:latest
          ports:
            - containerPort: 3000
              name: archerdb
            - containerPort: 9090
              name: metrics
          envFrom:
            - configMapRef:
                name: archerdb-config
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          command:
            - /bin/sh
            - -c
            - |
              REPLICA_INDEX=${POD_NAME##*-}
              ./archerdb start \
                --addresses=$ADDRESSES \
                --replica=$REPLICA_INDEX \
                /data/archerdb.db
          resources:
            requests:
              memory: "2Gi"
              cpu: "1"
            limits:
              memory: "4Gi"
              cpu: "2"
          volumeMounts:
            - name: data
              mountPath: /data
          livenessProbe:
            httpGet:
              path: /health/live
              port: 9090
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 9090
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

Deploy the manifests:

kubectl apply -f archerdb-config.yaml
kubectl apply -f archerdb-headless.yaml
kubectl apply -f archerdb-statefulset.yaml

Verification

# Check pod status
kubectl get pods -n archerdb -w

# Expected output after ~60 seconds:
# archerdb-0   1/1     Running   0          45s
# archerdb-1   1/1     Running   0          30s
# archerdb-2   1/1     Running   0          15s

# Check logs for cluster formation
kubectl logs -n archerdb archerdb-0 | grep -i "quorum"

# Verify cluster health
kubectl exec -n archerdb archerdb-0 -- curl -s localhost:9090/health/detailed

# Check metrics
kubectl exec -n archerdb archerdb-0 -- curl -s localhost:9090/metrics | grep archerdb_replica_role

Client Service

Expose ArcherDB to clients within the cluster:

# archerdb-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: archerdb
  namespace: archerdb
spec:
  ports:
    - port: 3000
      name: archerdb
  selector:
    app: archerdb

Clients connect to archerdb.archerdb.svc.cluster.local:3000.

Notes

  • Helm charts: For Helm-based deployment, see future releases.
  • Operator: A Kubernetes Operator for automated cluster management is planned for future releases.
  • For capacity planning in Kubernetes, see docs/capacity-planning.md.

Upgrade Procedures

Upgrade ArcherDB without downtime using rolling upgrades. This section covers version upgrades, not configuration changes (see Rolling Restart for config updates).

Pre-Upgrade Checklist

Before any upgrade:

Version Compatibility Matrix

From Version To Version Upgrade Path Notes
1.x.y 1.x.z Direct Patch upgrades always supported
1.x.y 1.(x+1).0 Direct Minor upgrades always supported
1.x.y 2.0.0 Via 1.latest Upgrade to latest 1.x first

Wire protocol compatibility:

  • Minor version upgrades maintain wire protocol compatibility
  • Major version upgrades may require all replicas to upgrade together

Rolling Upgrade Procedure

1. Identify current primary:

# Check which replica is primary
for node in node1 node2 node3; do
  echo -n "$node: "
  ssh $node "curl -s localhost:9090/metrics | grep archerdb_replica_role"
done

2. Upgrade followers first (one at a time):

# On each follower node:
# a. Stop the replica
systemctl stop archerdb

# b. Replace the binary
mv /usr/local/bin/archerdb /usr/local/bin/archerdb.old
cp /path/to/new/archerdb /usr/local/bin/archerdb
chmod +x /usr/local/bin/archerdb

# c. Start the replica
systemctl start archerdb

# d. Wait for replica to catch up
while true; do
  lag=$(curl -s localhost:9090/metrics | grep 'archerdb_replication_lag_ms' | awk '{print $2}')
  if [ "$lag" -lt 100 ]; then
    echo "Replica caught up (lag: ${lag}ms)"
    break
  fi
  echo "Waiting for catch-up (lag: ${lag}ms)..."
  sleep 5
done

3. Verify cluster health after each follower:

# Check quorum is maintained
./archerdb status --addresses=$ADDRESSES

# Check no errors in logs
journalctl -u archerdb --since "5 minutes ago" | grep -i error

4. Upgrade the primary last:

# On primary node:
# This triggers a view change - new primary elected from upgraded followers
systemctl stop archerdb

# Replace binary
mv /usr/local/bin/archerdb /usr/local/bin/archerdb.old
cp /path/to/new/archerdb /usr/local/bin/archerdb
chmod +x /usr/local/bin/archerdb

# Start - will rejoin as follower initially
systemctl start archerdb

Kubernetes Rolling Upgrade

# Update the image tag
kubectl set image statefulset/archerdb \
  archerdb=archerdb/archerdb:v1.2.0 \
  -n archerdb

# Watch rollout progress
kubectl rollout status statefulset/archerdb -n archerdb

# Kubernetes upgrades pods in reverse order (archerdb-2, then archerdb-1, then archerdb-0)
# ensuring followers upgrade before the primary

Post-Upgrade Verification

After all replicas upgraded:

Rollback Procedure

If issues occur during upgrade:

1. Stop the problematic replica:

systemctl stop archerdb

2. Restore the old binary:

mv /usr/local/bin/archerdb.old /usr/local/bin/archerdb

3. Start with old version:

systemctl start archerdb

4. For Kubernetes rollback with your deployment tooling:

kubectl rollout undo statefulset/archerdb -n archerdb

If external rollback fails or data corruption is suspected: See Disaster Recovery Procedures for external snapshot restoration.

Maintenance

Rolling Restart

To update ArcherDB without downtime:

# 1. Restart one replica at a time
# Start with followers, primary last

# Stop replica 2 (follower)
ssh node3 "systemctl stop archerdb"
ssh node3 "systemctl start archerdb"
# Wait for replica to catch up (check replication_lag_ms)

# Stop replica 1 (follower)
ssh node2 "systemctl stop archerdb"
ssh node2 "systemctl start archerdb"
# Wait for replica to catch up

# Stop replica 0 (primary)
# This triggers view change - new primary elected
ssh node1 "systemctl stop archerdb"
ssh node1 "systemctl start archerdb"

Data File Maintenance

# Check data file integrity
./archerdb verify /data/archerdb.db

# Compact data file (reduces disk usage)
./archerdb compact /data/archerdb.db

Performance Validation

Run periodic performance validation and keep a baseline for regressions:

# Start a local single-node cluster
./scripts/dev-cluster.sh start --nodes=1 --clean

# Built-in benchmark driver (smoke-scale settings)
./archerdb benchmark --cluster=0 --addresses=127.0.0.1:3001 --events=10000 --batch-size=500

# Multi-language benchmark suite (writes summary CSV)
./scripts/run_benchmarks.sh --events 10000 --batch-size 500 --cluster 127.0.0.1:3001

# Save a baseline for future comparison
cp benchmark-results/summary_*.csv benchmark-results/baseline.csv

The benchmark summary is stored under benchmark-results/ and can be compared to a baseline by re-running the script with --baseline benchmark-results/baseline.csv.

Gateway/Proxy Certificate Rotation

# 1. Deploy new certificates alongside old ones
# 2. Update config to use new certificates
# 3. Rolling restart all replicas
# 4. Remove old certificates

Troubleshooting

This section covers quick troubleshooting tips. For comprehensive diagnosis and resolution procedures, see the Troubleshooting Guide.

Connection Issues

Symptom: Clients can’t connect

Checklist:

  1. Check server is running: systemctl status archerdb
  2. Check port is open: netstat -tlnp | grep 3000
  3. Check firewall rules: iptables -L -n
  4. Check gateway/proxy TLS certificates (if applicable)
  5. Verify cluster_id matches between client and server

High Latency

Symptom: P99 latency > 100ms

Checklist:

  1. Check disk I/O: iostat -x 1
  2. Check for view changes: curl localhost:9090/metrics | grep view_changes
  3. Check replication lag: curl localhost:9090/metrics | grep replication_lag
  4. Check batch sizes (too small = overhead, too large = queuing)
  5. Check if compaction is running

Cluster Won’t Start

Symptom: Replicas won’t form quorum

Checklist:

  1. Verify all replicas use same cluster_id
  2. Check network connectivity between nodes
  3. Check for clock skew: chronyc tracking
  4. Verify data files aren’t corrupted: ./archerdb verify
  5. Check logs for specific errors

Out of Disk Space

Symptom: Writes failing with “out of space”

Immediate Actions:

  1. Check disk usage: df -h
  2. Identify largest files: du -sh /data/*
  3. If TTL is configured, wait for expiration
  4. Run compaction: ./archerdb compact
  5. Add disk capacity or archive old data

Split Brain Prevention

ArcherDB uses Viewstamped Replication which prevents split brain by design:

  • Requires majority (2 of 3) to commit
  • No “split brain” possible with proper configuration

If replicas are partitioned:

  • Majority partition continues operating
  • Minority partition rejects writes (returns cluster_unavailable)
  • After partition heals, minority catches up automatically

Emergency Procedures

Cluster Recovery from Total Failure

If all replicas fail simultaneously:

# 1. Stop all replicas
systemctl stop archerdb  # on all nodes

# 2. Verify data files
./archerdb verify /data/archerdb.db  # on all nodes

# 3. Start the replica with the most recent data first
# Check timestamps in logs or data file metadata

# 4. Start remaining replicas - they will sync from the first one

Recovering from Corrupted Replica

# 1. Stop the corrupted replica
systemctl stop archerdb

# 2. Remove corrupted data file
mv /data/archerdb.db /data/archerdb.db.corrupted

# 3. Re-format and rejoin cluster
./archerdb format --cluster=12345 --replica=N --replica-count=3 /data/archerdb.db
systemctl start archerdb

# 4. Replica will sync from healthy replicas

Emergency Cluster Shutdown

# If immediate shutdown is required
# (e.g., security incident, runaway process)

# Send SIGKILL to all replicas
pkill -9 archerdb

# Note: This may leave some operations uncommitted
# but data integrity is preserved due to write-ahead logging

Contact and Escalation

Issue Type Contact SLA
Cluster unavailable On-call 15 min response
High latency On-call 1 hour response
Disk space Team lead 4 hour response
Security incident Security team Immediate

Appendix: Common Commands

# Check cluster health
./archerdb status --addresses=node1:3000

# List active sessions
./archerdb sessions --addresses=node1:3000

# Force leader election (use with caution)
./archerdb step-down --addresses=primary:3000

# Export metrics snapshot
curl -s localhost:9090/metrics > metrics-$(date +%Y%m%d).prom

Core Operations

Document Description
Troubleshooting Guide Comprehensive diagnosis and resolution procedures
Capacity Planning Sizing guidelines for hardware and configuration
LSM Tuning Storage engine performance optimization

Backup and Recovery

ArcherDB has built-in backup upload to local filesystem, S3 (or S3-compatible providers — MinIO, R2, Backblaze, LocalStack), GCS via interop, and Azure Blob Storage. Enable at startup:

archerdb start \
  --backup-enabled \
  --backup-provider=s3 \
  --backup-region=us-east-1 \
  --backup-bucket=my-archerdb-backups \
  data.archerdb

S3 / GCS / Azure each accept their own endpoint, credential, and url-style flags — see Backup Operations for the full per-provider matrix. Restore via archerdb restore from any supported provider.

Monitor backup health via the metrics endpoint:

  • archerdb_backup_blocks_uploaded_total — cumulative blocks durably uploaded.
  • archerdb_backup_lag_blocks — pending block uploads.
  • archerdb_backup_failures_total — upload failures.
  • archerdb_storage_space_exhausted — gauge that flips to 1 when the replica is currently rejecting client writes due to a recent storage capacity event.

Platform snapshots remain available as defense-in-depth alongside the built-in path.

Document Description
Backup Operations Built-in providers, credentials, retention, external snapshot procedures
Disaster Recovery DR planning, RTO/RPO targets, recovery procedures
Upgrade Guide Rolling upgrade procedures, external rollback planning, health checks

Alert Runbooks

Runbook Alerts Covered
Replica Down ArcherDBReplicaDown
View Changes ArcherDBViewChangeFrequent
Index Degraded ArcherDBIndexDegraded
High Read Latency ArcherDBReadLatencyP99Warning/Critical
High Write Latency ArcherDBWriteLatencyP99Warning/Critical, ArcherDBHighLatency
Disk Capacity ArcherDBDiskSpaceWarning/Critical, ArcherDBDiskFillPrediction
Compaction Backlog ArcherDBCompactionBacklog
Edit this page

Capacity Planning Guide

This guide helps you size ArcherDB deployments for your expected workload.

Table of Contents

Quick Reference

Memory Requirements

Entity Count Index Memory Recommended RAM Minimum RAM
1 Million ~137 MB 4 GB 2 GB
10 Million ~1.37 GB 8 GB 4 GB
100 Million ~13.7 GB 32 GB 24 GB
500 Million ~68.6 GB 128 GB 96 GB
1 Billion ~137 GB 192 GB 160 GB

Disk Requirements (Latest Position Only)

Entity Count Data Size With 5x History With 10x History
1 Million 128 MB 640 MB 1.28 GB
10 Million 1.28 GB 6.4 GB 12.8 GB
100 Million 12.8 GB 64 GB 128 GB
1 Billion 128 GB 640 GB 1.28 TB

Memory Planning

Index Memory Formula

ArcherDB maintains a RAM index for O(1) entity lookups. The raw IndexEntry is 64 bytes (cache-line aligned); capacity planning uses 96 bytes per slot because ArcherDB also reserves spatial scan-helper arrays per slot.

Index Memory = (Entity Count / Load Factor) × 96 bytes

Where:
- Target Load Factor = 0.70 (70% capacity utilization)
- 96 bytes = 64-byte IndexEntry + per-slot scan-helper arrays

Example for 1 billion entities:

Capacity = 1,000,000,000 / 0.70 = ~1,428,571,428 slots
Memory = 1,428,571,428 × 96 bytes = ~137 GB

RAM Allocation Breakdown

For a 192 GB system targeting 1 billion entities:

Component Allocation Purpose
Primary Index ~137 GB O(1) entity lookups and scan-helper arrays
Block Cache 4-16 GB LSM tree read caching
Query Buffers 1-2 GB Result set assembly
VSR Buffers 1-2 GB Replication pipeline
Operating System 16-32 GB Kernel, page cache

Memory Headroom Requirements

Always provision more RAM than the raw index size:

Recommended RAM = Index Memory × 1.4

Reasons:
- Hash table performance degrades near capacity
- Memory fragmentation overhead
- Operating system buffers
- Query result buffers
- Grid cache for frequently accessed blocks

Large Page Support

For optimal performance with large indexes:

# Enable Transparent Huge Pages (Linux)
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# Or allocate explicit huge pages (2MB pages)
# For 100GB index, allocate 51,200 huge pages
echo 51200 > /proc/sys/vm/nr_hugepages

Large pages reduce TLB misses during random index access.

Disk Planning

GeoEvent Storage

Each GeoEvent record is 128 bytes:

Disk Space (Latest Only) = Entity Count × 128 bytes

Historical Retention Multiplier

Disk usage increases based on how often entities are updated:

Workload Type Updates per Entity Example Storage Multiplier
Low frequency 1-5 updates Asset tracking 1-5×
Medium frequency 5-20 updates Fleet management 5-20×
High frequency 20+ updates Real-time delivery 20-50×

Example calculations for 1 billion entities:

Low frequency (monthly position updates):
  128GB × 5 = 640 GB SSD required

Medium frequency (hourly updates):
  128GB × 10 = 1.28 TB SSD required

High frequency (every 30 seconds):
  128GB × 30 = 3.84 TB SSD required

TTL and Disk Reclamation

If using TTL (time-to-live) for automatic data expiration:

Effective Storage = (Event Rate × TTL Duration) × 128 bytes

Example:

Event Rate: 10,000 events/second
TTL: 30 days (2,592,000 seconds)

Effective Storage = 10,000 × 2,592,000 × 128 bytes
                 = 3.32 TB

Disk Performance Requirements

Workload Sequential Read Random Read Sequential Write
Development >500 MB/s >10K IOPS >200 MB/s
Production >2 GB/s >50K IOPS >1 GB/s
High Performance >5 GB/s >100K IOPS >3 GB/s

Recommended: NVMe SSDs with >3 GB/s sequential read for production workloads.

Data File Size Limits

Maximum data file size: 16 TB
Maximum events per file: ~137 billion (at 128 bytes each)

Hardware Recommendations

Development Environment

For development and testing (up to 300 million entities):

Component Specification
CPU 8 cores, x86-64 with AES-NI
RAM 32 GB
Disk 500 GB NVMe SSD
Network 1 Gbps

Production (1 Billion Entities)

For production deployments targeting 1 billion entities:

Component Specification
CPU 16+ cores, x86-64 with AVX2
RAM 128 GB (ECC recommended)
Disk 1 TB+ NVMe SSD (3+ GB/s)
Network 10 Gbps between replicas

High Performance (>1M Events/sec)

For maximum throughput requirements:

Component Specification
CPU 32+ cores, Intel Sapphire Rapids or AMD Zen 4
RAM 256 GB (ECC required)
Disk 2 TB+ NVMe Gen4/Gen5 (5+ GB/s)
Network 25-100 Gbps

CPU Features Required

  • AES-NI: Required for Aegis-128L checksumming
  • AVX2: Improves SIMD operations (recommended)
  • RDTSCP: Timestamp counter for profiling

Scaling Scenarios

Scenario 1: Fleet Management (100K Vehicles)

Entities: 100,000 vehicles
Update frequency: Every 10 seconds
Retention: 90 days

Memory:
  Index = (100,000 / 0.7) × 96 = ~13.7 MB
  Recommended RAM: 4 GB

Disk:
  Events/day = 100,000 × 8,640 = 864M events
  90-day retention = 77.8B events × 128 bytes = ~10 TB
  Recommended: 12 TB NVMe SSD

Throughput:
  Write rate = 100,000 / 10 = 10,000 events/sec
  Single replica sufficient

Scenario 2: Mobile App (10M Users)

Entities: 10,000,000 users
Update frequency: Every 60 seconds (when app active)
Active ratio: 10% at any time
Retention: 7 days

Memory:
  Index = (10,000,000 / 0.7) × 96 = ~1.37 GB
  Recommended RAM: 8 GB

Disk:
  Active users: 1,000,000
  Events/day = 1,000,000 × 1,440 = 1.44B events
  7-day retention = 10.1B events × 128 bytes = ~1.3 TB
  Recommended: 2 TB NVMe SSD

Throughput:
  Peak write rate = 1,000,000 / 60 = ~16,700 events/sec
  3-replica cluster recommended

Scenario 3: IoT Platform (100M Devices)

Entities: 100,000,000 devices
Update frequency: Varies (1 min to 1 hour)
Average: 15-minute intervals
Retention: 30 days

Memory:
  Index = (100,000,000 / 0.7) × 96 = ~13.7 GB
  Recommended RAM: 32 GB

Disk:
  Events/day = 100,000,000 × 96 = 9.6B events
  30-day retention = 288B events × 128 bytes = ~37 TB
  Data tiering required (recent on NVMe, archived on HDD/S3)

Throughput:
  Average write rate = 100,000,000 / 900 = ~111,000 events/sec
  5-replica cluster with sharding

Scenario 4: Global Logistics (1B Shipments)

Entities: 1,000,000,000 shipments (cumulative)
Active: 50,000,000 in-transit
Update frequency: Every 5 minutes when moving
Retention: Indefinite for audit

Memory:
  Index = (1,000,000,000 / 0.7) × 96 = ~137 GB
  Recommended RAM: 192 GB per node

Disk:
  Active events/day = 50,000,000 × 288 = 14.4B events
  Plus completions: ~10M/day
  Growing storage: ~1.8 TB/day
  Multi-region with S3 archival required

Throughput:
  Write rate = 50,000,000 / 300 = ~166,000 events/sec
  Multi-region deployment with geo-sharding

Monitoring Capacity

Key Metrics to Watch

Metric Warning Critical Action
archerdb_index_load_factor > 0.6 > 0.75 Scale or rebuild
archerdb_disk_usage_bytes > 70% > 85% Add storage
archerdb_memory_usage_bytes > 75% > 90% Add RAM or scale
archerdb_index_tombstone_ratio > 0.1 > 0.3 Schedule rebuild

Prometheus Alerts

groups:
  - name: archerdb_capacity
    rules:
      # Index approaching capacity
      - alert: IndexCapacityWarning
        expr: archerdb_index_load_factor > 0.6
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Index load factor {{ $value | humanizePercentage }}"
          action: "Plan capacity increase within 2 weeks"

      - alert: IndexCapacityCritical
        expr: archerdb_index_load_factor > 0.75
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Index at critical capacity"
          action: "Immediate action required"

      # Disk space
      - alert: DiskSpaceWarning
        expr: archerdb_disk_usage_bytes / archerdb_disk_total_bytes > 0.7
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Disk usage {{ $value | humanizePercentage }}"
          action: "Plan storage expansion"

      - alert: DiskSpaceCritical
        expr: archerdb_disk_usage_bytes / archerdb_disk_total_bytes > 0.85
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Disk critically low"
          action: "Immediate storage expansion required"

      # Memory
      - alert: MemoryWarning
        expr: archerdb_memory_usage_bytes / node_memory_total_bytes > 0.75
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Memory usage {{ $value | humanizePercentage }}"

Capacity Dashboard

Create a Grafana dashboard showing:

  1. Current vs. Maximum Capacity

    • Entity count / max entities
    • Disk used / disk available
    • Memory used / memory available
  2. Growth Trends

    • Entity count over 30 days
    • Disk growth rate (GB/day)
    • Event ingestion rate
  3. Resource Efficiency

    • Index load factor
    • Query latency trends
    • Compaction throughput
  4. Projected Exhaustion

    • Days until 80% index capacity
    • Days until 90% disk capacity
    • Required expansion timeline

Growth Planning

Capacity Planning Worksheet

Use this worksheet when planning deployments:

1. Entity Estimation
   ─────────────────
   Current entities:      ____________
   Growth rate/month:     ____________ %
   Target timeline:       ____________ months
   Projected entities:    ____________

2. Memory Calculation
   ─────────────────
   Projected entities:    ____________
   ÷ Load factor (0.7):   ÷ 0.7
   × Slot budget (96B):   × 96
   = Index memory:        ____________ GB
   × Headroom (1.4):      × 1.4
   = Recommended RAM:     ____________ GB

3. Disk Calculation
   ─────────────────
   Projected entities:    ____________
   × Event size (128B):   × 128
   = Base storage:        ____________ GB
   × History multiplier:  × ____________
   = Required storage:    ____________ GB
   × Safety margin (1.2): × 1.2
   = Recommended disk:    ____________ GB

4. Throughput Calculation
   ──────────────────────
   Peak concurrent users: ____________
   × Events per second:   × ____________
   = Required throughput: ____________ events/sec
   ÷ Per-replica capacity (10K): ÷ 10,000
   = Minimum replicas:    ____________

Scaling Decision Tree

                    Entity Growth?
                         │
         ┌───────────────┴───────────────┐
         ▼                               ▼
    < 20% annually                  > 20% annually
         │                               │
         ▼                               ▼
    Vertical scaling              Horizontal scaling
    (larger nodes)                (more nodes)
         │                               │
         │                               │
    ┌────┴────┐                    ┌─────┴─────┐
    ▼         ▼                    ▼           ▼
 RAM      Storage              Sharding    Multi-region
upgrade   expansion              by         deployment
                             group_id

Pre-Scaling Checklist

Before scaling capacity:

Capacity Review Schedule

Deployment Size Review Frequency Growth Threshold
< 100M entities Quarterly > 50% capacity
100M - 500M Monthly > 60% capacity
> 500M Weekly > 70% capacity

Appendix: Capacity Formulas

Memory

Index Memory (GB) = (entities / 0.7) × 96 / 1,073,741,824
Recommended RAM (GB) = Index Memory × 1.4

Disk

Base Storage (GB) = entities × 128 / 1,073,741,824
With History (GB) = Base Storage × (1 + updates_per_entity)
With TTL (GB) = event_rate_per_sec × ttl_seconds × 128 / 1,073,741,824

Throughput

Events per Second = concurrent_entities / update_interval_seconds
Required Replicas = events_per_second / 10,000 (rounded up)

Network

Replication Bandwidth (Mbps) = events_per_second × 128 × 8 / 1,000,000
With 3 replicas: Total = Replication Bandwidth × 2 (primary to backups)
Edit this page

Multi-Region Deployment Guide

This document is currently a design/reference guide, not a GA server deployment path.

The current archerdb start CLI does not accept server-side multi-region runtime flags. The examples below describe a proposed future runtime shape and should be treated as architecture and planning material only.

This guide explains the intended ArcherDB multi-region configuration with async replication between a primary region and follower regions.

Overview

Multi-region deployment provides:

  • Geo-distributed reads: Low-latency reads from the nearest region
  • Disaster recovery: Data replicated across regions
  • Read scaling: Follower regions handle read traffic

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                          Primary Region (us-east-1)                  │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐                        │
│  │ Replica 0 │  │ Replica 1 │  │ Replica 2 │  ← Writes + Reads      │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘                        │
│        │              │              │                               │
│        └──────────────┼──────────────┘                               │
│                       │                                              │
│                       │ WAL Shipping (async)                         │
│                       ▼                                              │
└─────────────────────────────────────────────────────────────────────┘
                        │
         ┌──────────────┴──────────────┐
         │                             │
         ▼                             ▼
┌─────────────────────┐    ┌─────────────────────┐
│ Follower (eu-west-1)│    │ Follower (ap-south-1)│
│  ┌─────────────┐    │    │  ┌─────────────┐    │
│  │   Replica   │    │    │  │   Replica   │    │
│  └─────────────┘    │    │  └─────────────┘    │
│  Read-only queries  │    │  Read-only queries  │
└─────────────────────┘    └─────────────────────┘

Prerequisites

  • ArcherDB binary on all nodes
  • Network connectivity between regions (TCP port 3000)
  • S3 bucket for cross-region WAL shipping (optional, for high-latency links)

Deployment Steps

Step 1: Deploy Primary Region

First, deploy a standard 3-replica cluster in your primary region:

# Format data files on each node
./archerdb format --cluster=12345 --replica=0 --replica-count=3 /data/archerdb.db  # node 1
./archerdb format --cluster=12345 --replica=1 --replica-count=3 /data/archerdb.db  # node 2
./archerdb format --cluster=12345 --replica=2 --replica-count=3 /data/archerdb.db  # node 3

Future runtime sketch for the primary cluster configuration (not accepted by the current archerdb start CLI):

# On all primary region nodes
archerdb start \
  --addresses=10.0.1.1:3000,10.0.1.2:3000,10.0.1.3:3000 \
  --region-role=primary \
  --follower-regions=10.0.2.1:3001,10.0.3.1:3001 \
  /data/archerdb.db

Primary Configuration Options (future runtime design):

Flag Description
--region-role=primary Designates this cluster as the primary region
--follower-regions=<endpoints> Comma-separated list of follower endpoints for WAL shipping

Step 2: Deploy Follower Regions

Future runtime sketch for each follower region (not accepted by the current archerdb start CLI):

# Format the follower data file
archerdb format --cluster=12345 --replica=0 --replica-count=1 /data/archerdb.db

# Start as follower
archerdb start \
  --addresses=10.0.2.1:3001 \
  --region-role=follower \
  --primary-region=10.0.1.1:3000 \
  /data/archerdb.db

Follower Configuration Options (future runtime design):

Flag Description
--region-role=follower Designates this node as a read-only follower
--primary-region=<endpoint> Primary region endpoint for WAL shipping

Step 3: Verify Replication

Check replication status on the primary:

./archerdb repl --cluster=12345 --addresses=10.0.1.1:3000 --command="status"

Expected output:

Cluster: 12345
Role: primary
Followers:
  - eu-west-1 (10.0.2.1:3001): lag=15ms, ops_behind=3
  - ap-south-1 (10.0.3.1:3001): lag=120ms, ops_behind=25

WAL Shipping Transports

Direct TCP (Default)

Best for low-latency inter-region links (<100ms RTT):

--region-role=primary \
--follower-regions=10.0.2.1:3001,10.0.3.1:3001

S3 Relay

For high-latency or unreliable links, use S3 as an intermediate buffer:

# Primary
--region-role=primary \
--replication-transport=s3 \
--replication-bucket=my-replication-bucket \
--replication-prefix=prod/wal

# Follower
--region-role=follower \
--primary-region=s3://my-replication-bucket/prod/wal

S3 Configuration:

Flag Description
--replication-transport=s3 Use S3 for WAL shipping
--replication-bucket=<bucket> S3 bucket name
--replication-prefix=<prefix> S3 key prefix for WAL files

Client Configuration

Routing Writes to Primary

All writes must go to the primary region. SDKs automatically handle this:

import archerdb

# Connect to any region - SDK routes writes to primary
client = archerdb.GeoClientSync(
    cluster_id=12345,
    addresses=["10.0.2.1:3001"]  # Follower address
)

# This write automatically routes to primary
batch = client.create_batch()
batch.insert(archerdb.create_geo_event(...))
batch.submit()  # Routed to primary via follower

Reading from Followers

For low-latency reads, connect directly to the nearest follower:

# Connect to nearest follower for reads
client = archerdb.GeoClientSync(
    cluster_id=12345,
    addresses=["10.0.2.1:3001"],  # EU follower
    prefer_follower_reads=True
)

# Reads served locally from follower
result = client.query_radius(51.5074, -0.1278, 1000)

Staleness Tolerance

Configure maximum acceptable staleness for reads:

from archerdb import OperationOptions

# Allow reads up to 5 seconds behind primary
options = OperationOptions(max_staleness_ms=5000)
result = client.query_radius(lat, lon, radius, options=options)

# Check actual staleness
print(f"Read staleness: {result.staleness_ns / 1e6:.2f}ms")

Monitoring

Replication Metrics

Monitor these Prometheus metrics:

# Replication lag (operations behind)
archerdb_replication_lag_ops{region="eu-west-1"} 15

# Replication lag (time)
archerdb_replication_lag_seconds{region="eu-west-1"} 0.050

# Ship queue depth
archerdb_ship_queue_depth{region="eu-west-1"} 100

# Shipping errors
archerdb_ship_errors_total{region="eu-west-1",error="timeout"} 5

Health Endpoints

Check region health via HTTP:

# Primary health
curl http://10.0.1.1:8080/health/region

# Response
{
  "role": "primary",
  "followers": [
    {"region": "eu-west-1", "lag_ms": 15, "status": "healthy"},
    {"region": "ap-south-1", "lag_ms": 120, "status": "healthy"}
  ]
}

Failover Procedures

Planned Failover

For maintenance or region migration:

  1. Stop writes to primary region
  2. Wait for followers to catch up (lag → 0)
  3. Promote follower to primary:
    ./archerdb promote --cluster=12345 --addresses=10.0.2.1:3001
  4. Reconfigure old primary as follower
  5. Update client configuration with new primary

Unplanned Failover

If primary region fails:

  1. Identify most caught-up follower:

    ./archerdb status --cluster=12345 --addresses=10.0.2.1:3001
    # Check commit_op to find most advanced follower
  2. Force promote the best follower:

    ./archerdb promote --force --cluster=12345 --addresses=10.0.2.1:3001

    Warning: Force promotion may lose operations not yet replicated.

  3. Reconfigure remaining followers to point to new primary

Error Handling

Follower Errors

Error Code Name Description Action
213 FOLLOWER_READ_ONLY Write attempted on follower Route to primary
214 STALE_FOLLOWER Follower too far behind Wait or use primary
215 PRIMARY_UNREACHABLE Cannot connect to primary Check network/failover
216 REPLICATION_TIMEOUT Replication timeout Retry or check lag

SDK Error Handling

from archerdb import MultiRegionException, MultiRegionError

try:
    batch.submit()
except MultiRegionException as e:
    if e.error == MultiRegionError.FOLLOWER_READ_ONLY:
        # Redirect write to primary
        pass
    elif e.error == MultiRegionError.STALE_FOLLOWER:
        # Data too stale, retry on primary
        pass

Best Practices

  1. Region Selection: Place primary in the region with most write traffic
  2. Follower Count: 1-3 followers per region (more increases replication load)
  3. Network: Use dedicated inter-region links or VPN for replication
  4. Monitoring: Alert on replication_lag_seconds > 5
  5. Backups: Run external snapshot pipelines from the primary region
  6. Testing: Regularly test failover procedures

Troubleshooting

High Replication Lag

  1. Check network latency between regions
  2. Verify follower has sufficient CPU/IO capacity
  3. Consider S3 transport for high-latency links
  4. Check ship_queue_depth metric for backpressure

Follower Not Receiving Updates

  1. Verify --primary-region endpoint is correct
  2. Check firewall allows TCP 3000/3001 between regions
  3. Check primary logs for shipping errors
  4. Verify S3 bucket permissions (if using S3 transport)
Edit this page

Backup Operations

ArcherDB has built-in backup upload to local filesystem, S3 (or S3-compatible endpoints — MinIO, R2, Backblaze, LocalStack), GCS via interop, and Azure Blob Storage. All four providers are end-to-end proven via integration tests and have CI lanes (Backup Restore, Backup Restore S3, Backup Restore Azure, and their Round-trip variants) that run on every PR.

Operators can still combine the built-in path with platform snapshots for extra redundancy; the two are not mutually exclusive.

Built-in Backup Providers

Provider Config Credentials
local --backup-provider=local --backup-bucket=/mnt/backups filesystem write access
s3 --backup-provider=s3 --backup-bucket=<name> [--backup-endpoint=<url> --backup-url-style=path] AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY or --backup-access-key-id / --backup-secret-access-key
gcs --backup-provider=gcs --backup-bucket=<name> (uses storage.googleapis.com over the S3-compatible Interop API) HMAC key issued via Cloud Storage → Settings → Interoperability
azure --backup-provider=azure --backup-bucket=<container> --backup-access-key-id=<account> --backup-secret-access-key=<base64-key> Azure storage account + SharedKey; SAS tokens supported on the restore side

See Disaster Recovery for the matching restore procedures.

Strategy

Use a layered approach:

  1. Replica redundancy for high availability
  2. Built-in backup upload (S3/GCS/Azure/local) for durable off-host copies
  3. Volume/object snapshots or cross-region immutable copies for defense-in-depth
  4. Off-site retention lock for regulated workloads
  • Persistent volume snapshots (cloud block storage or CSI snapshot)
  • Host-level filesystem snapshots
  • Replicated encrypted object-store archives

Retention Policy Example

  • Daily snapshots retained 14 days
  • Weekly snapshots retained 8 weeks
  • Monthly snapshots retained 12 months
  • Immutable retention lock for regulated workloads

Backup Procedure (Generic)

  1. Confirm cluster health and quorum
  2. Trigger platform snapshot for all replica data volumes
  3. Replicate snapshots/artifacts to secondary region/account
  4. Record snapshot IDs, timestamps, and checksums in runbook

Restore Procedure (Generic)

  1. Provision replacement nodes/volumes
  2. Restore data volumes from selected snapshot set
  3. Start replicas and rejoin cluster using standard startup/recover flow
  4. Validate application-level correctness (smoke + integrity checks)

Verification Cadence

  • Weekly: snapshot job success audit
  • Monthly: restore-to-staging drill
  • Quarterly: full-region recovery exercise

Evidence to Keep

  • Snapshot IDs and retention policy proof
  • Restore test logs and timing (RTO/RPO)
  • Access audit logs for backup and key operations
  • Incident notes for failed backup or restore events
Edit this page

Disaster Recovery Procedures

This guide covers disaster recovery for ArcherDB using:

  • Consensus replication for node/replica failures
  • ArcherDB’s built-in backup/restore to S3, GCS, Azure Blob, or a local filesystem (see Backup Operations for provider configuration)
  • Optional external backup/snapshot tooling as defense-in-depth

Recovery Objectives

Define and track per environment:

  • RTO (time to recover service)
  • RPO (acceptable data loss window from backup snapshots)

Failure Classes

  1. Single replica loss (quorum remains)
  2. Minority replica loss (quorum remains)
  3. Majority loss (quorum lost)
  4. Full cluster loss
  5. Storage corruption

Replica Loss Recovery

When a data file is lost, recover with recover (not format):

./archerdb recover \
  --cluster=0 \
  --addresses=127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 \
  --replica=2 \
  --replica-count=3 \
  /data/0_2.archerdb

Then start the replica normally and allow it to catch up.

Full Cluster Loss Recovery

  1. Provision replacement infrastructure
  2. Restore replica data from external snapshots
  3. Start replicas with original cluster metadata
  4. Validate quorum, health endpoints, and smoke tests
  5. Re-enable traffic after validation gates pass

Data Corruption Recovery

  1. Isolate affected replica(s)
  2. Preserve forensic artifacts and logs
  3. Restore from last known-good external snapshot
  4. Rejoin cluster and validate integrity

Required Runbooks

  • External snapshot creation and retention
  • External snapshot restore (regional and cross-region)
  • Key and access-control recovery for encrypted storage
  • Traffic cutover/rollback procedures

Drill Schedule

  • Monthly: restore test in staging
  • Quarterly: production-like DR exercise
  • Post-incident: targeted replay and remediation validation

Evidence

Keep:

  • Snapshot IDs and retention proof
  • Restore timings vs RTO/RPO targets
  • Validation logs from health/smoke/integrity checks
  • Postmortem and corrective action tickets
Edit this page

ArcherDB Upgrade Guide

This guide provides procedures for safely upgrading ArcherDB clusters with minimal downtime, using archerdb upgrade for status and dry-run planning and your deployment tooling for live rollout and rollback.

Table of Contents

Overview

ArcherDB upgrades follow a rolling upgrade philosophy designed for zero-downtime deployments:

  1. One node at a time - Never upgrade multiple replicas simultaneously
  2. Followers first, primary last - Minimizes disruption to write operations
  3. Health-based rollback planning - Thresholds for your deployment tooling to decide when to stop or roll back
  4. Version compatibility - Backwards-compatible data format between versions

Upgrade Order

The upgrade process follows this strict order:

1. Identify primary replica
2. For each follower replica:
   a. Upgrade the follower
   b. Wait for catch-up (replication lag < threshold)
   c. Verify health checks pass
   d. Continue to next follower
3. Upgrade primary last
4. Verify cluster health

This order ensures:

  • Quorum is maintained throughout the upgrade
  • Write availability is preserved until the final step
  • External deployment tooling can roll back to the previous version if issues occur

Pre-Upgrade Checklist

Before starting any upgrade, complete this checklist:

Required Checks

  • archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

Pre-Upgrade Health Check

Run this command to verify cluster readiness:

# Check cluster status and identify primary
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# Expected output shows:
# - All replicas healthy
# - Primary identified
# - Replication lag < 100ms

Version Compatibility

ArcherDB follows the TigerBeetle model for version compatibility:

Upgrade Rules

  1. Sequential upgrades: Each version specifies the oldest compatible source version
  2. Skip versions: May require intermediate upgrades (check CHANGELOG)
  3. Data format: Backwards compatible within major versions
  4. Wire protocol:
    • Minor versions (1.x.y -> 1.x.z): Always compatible
    • Major versions (1.x -> 2.x): May require simultaneous upgrade

Checking Compatibility

# Check current versions
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# Dry-run to check compatibility
archerdb upgrade start --addresses=node1:3000,node2:3000,node3:3000 \
  --target-version=1.2.0 --dry-run

Version Compatibility Matrix

From Version To Version Compatible Notes
1.0.x 1.1.x Yes Direct upgrade supported
1.1.x 1.2.x Yes Direct upgrade supported
1.0.x 1.2.x Yes May upgrade directly or via 1.1.x
1.x 2.0 Check Review CHANGELOG for breaking changes

Upgrade Procedures

Bare Metal Upgrade

Step 1: Verify Current State

# Check cluster health and identify primary
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# Record output showing:
# - Primary: node1:3000 (replica 0)
# - Followers: node2:3000, node3:3000
# - All replicas: healthy

Step 2: Download New Binary

# On each node, download the new version
wget https://releases.archerdb.io/v1.2.0/archerdb-linux-amd64
chmod +x archerdb-linux-amd64

Step 3: Upgrade Followers (One at a Time)

For each follower node (NOT the primary):

# On follower node (e.g., node2)

# 1. Stop the current process gracefully
systemctl stop archerdb
# Or: kill -TERM $(pidof archerdb)

# 2. Replace the binary
mv /usr/local/bin/archerdb /usr/local/bin/archerdb.bak
mv archerdb-linux-amd64 /usr/local/bin/archerdb

# 3. Start with new version
systemctl start archerdb

# 4. Wait for catch-up and health check
# Monitor logs for "replication caught up" message
journalctl -u archerdb -f

After each follower upgrade, verify:

# Check follower is healthy and caught up
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# Expected: follower shows new version, healthy, low replication lag

Step 4: Upgrade Primary Last

# On primary node (node1)

# 1. Stop primary (triggers leader election among upgraded followers)
systemctl stop archerdb

# 2. Replace binary
mv /usr/local/bin/archerdb /usr/local/bin/archerdb.bak
mv archerdb-linux-amd64 /usr/local/bin/archerdb

# 3. Start with new version
systemctl start archerdb

# 4. Verify new primary elected and old primary rejoins as follower
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

Kubernetes Upgrade

For Kubernetes deployments, upgrades are managed through StatefulSet image updates. The ArcherDB CLI provides monitoring and guidance.

Step 1: Check Current State

# Check cluster status
archerdb upgrade status --addresses=archerdb-0:3000,archerdb-1:3000,archerdb-2:3000

# Check current image
kubectl get statefulset archerdb -n archerdb -o jsonpath='{.spec.template.spec.containers[0].image}'

Step 2: Update Image Tag

# Update the image tag
kubectl set image statefulset/archerdb \
  archerdb=archerdb/archerdb:v1.2.0 \
  -n archerdb

Step 3: Watch Rolling Update

Kubernetes StatefulSet performs a rolling update automatically:

# Watch rollout progress
kubectl rollout status statefulset/archerdb -n archerdb

# Monitor pod restarts
kubectl get pods -n archerdb -w

# Check upgrade status via CLI
archerdb upgrade status --addresses=archerdb-0:3000,archerdb-1:3000,archerdb-2:3000

Step 4: Verify Completion

# Verify all pods running new version
kubectl get pods -n archerdb -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'

# Verify cluster health
archerdb upgrade status --addresses=archerdb-0:3000,archerdb-1:3000,archerdb-2:3000

Using the Upgrade CLI

The upgrade CLI provides status inspection and dry-run planning. The live rollout is still performed by your external deployment tooling:

# Dry-run first to see the upgrade plan
archerdb upgrade start --addresses=node1:3000,node2:3000,node3:3000 \
  --target-version=1.2.0 --dry-run

# Generate a dry-run plan with custom thresholds
archerdb upgrade start --addresses=node1:3000,node2:3000,node3:3000 \
  --target-version=1.2.0 \
  --p99-threshold-x10=30 \      # 3.0x baseline triggers rollback
  --error-threshold-x10=5 \      # 0.5% error rate triggers rollback
  --catchup-timeout=600          # 10 minute catchup timeout

Use the resulting plan to drive a follower-first rollout in your service manager, Kubernetes controller, or other deployment system.

Health-Based Planning Thresholds

ArcherDB evaluates these thresholds during dry-run planning and live status checks. Actual rollback is performed by your deployment tooling.

Rollout Risk Triggers

Treat any of these conditions as rollout blockers:

Condition Default Threshold Description
Readiness probe failure 3 consecutive failures Replica health endpoint returns non-200
P99 latency spike 2x baseline P99 latency exceeds double pre-upgrade baseline
Absolute P99 latency 100ms P99 latency exceeds absolute maximum
Error rate 1% Request error rate exceeds threshold
Catchup timeout 300 seconds Replica fails to catch up within timeout

Customizing Thresholds

# Stricter thresholds (more sensitive to degradation)
archerdb upgrade start --addresses=... --target-version=1.2.0 \
  --dry-run \
  --p99-threshold-x10=15 \       # 1.5x baseline
  --error-threshold-x10=5 \       # 0.5% errors
  --catchup-timeout=180           # 3 minutes

# Looser thresholds (more tolerant)
archerdb upgrade start --addresses=... --target-version=1.2.0 \
  --dry-run \
  --p99-threshold-x10=50 \       # 5.0x baseline
  --error-threshold-x10=20 \      # 2.0% errors
  --catchup-timeout=600           # 10 minutes

External Rollback

Bare Metal Rollback

# Check upgrade status
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# ArcherDB does not perform rollback itself; use your service manager or deploy tool

Or roll back manually on each node with your service manager or deployment tooling:

# On each upgraded node (primary first if upgraded):

# 1. Stop the process
systemctl stop archerdb

# 2. Restore old binary
mv /usr/local/bin/archerdb.bak /usr/local/bin/archerdb

# 3. Start with old version
systemctl start archerdb

Kubernetes Rollback

# Rollback to previous revision
kubectl rollout undo statefulset/archerdb -n archerdb

# Or rollback to specific revision
kubectl rollout undo statefulset/archerdb -n archerdb --to-revision=2

# Watch rollback progress
kubectl rollout status statefulset/archerdb -n archerdb

Post-Upgrade Verification

After upgrade completes, verify these conditions:

Immediate Checks

# 1. All replicas running new version
archerdb upgrade status --addresses=node1:3000,node2:3000,node3:3000

# 2. Cluster health is normal
curl http://node1:9100/health/ready
curl http://node2:9100/health/ready
curl http://node3:9100/health/ready

# 3. Replication lag is minimal
curl http://node1:9100/metrics | grep archerdb_replication_lag

Performance Validation

# 4. Compare P99 latency to baseline
curl http://node1:9100/metrics | grep 'request_duration.*quantile="0.99"'

# 5. Check error rate
curl http://node1:9100/metrics | grep archerdb_request_errors

# 6. Verify throughput
curl http://node1:9100/metrics | grep archerdb_operations_total

Functional Validation

# 7. Test write operations
archerdb repl --addresses=node1:3000,node2:3000,node3:3000 --cluster=<cluster-id>
> INSERT INTO geo_events ...

# 8. Test read operations
> SELECT * FROM geo_events WHERE ...

Troubleshooting

Upgrade Stuck on Replica

Symptom: Upgrade status shows “waiting_for_catchup” for extended time

Diagnosis:

# Check replica logs
journalctl -u archerdb -f  # bare metal
kubectl logs archerdb-1 -n archerdb  # kubernetes

# Check replication lag
curl http://node2:9100/metrics | grep replication_lag

Solutions:

  1. Increase catchup timeout: --catchup-timeout=600
  2. Check network connectivity between replicas
  3. Verify disk I/O is not saturated
  4. Check if compaction is blocking catch-up

Rollback Fails to Complete

Symptom: External rollback command hangs or reports errors

Diagnosis:

# Check node status
archerdb upgrade status --addresses=...

# Check process status
systemctl status archerdb

Solutions:

  1. Manually stop and restart each replica with old binary
  2. Check for disk space issues
  3. Verify network connectivity

Version Incompatibility Error

Symptom: Upgrade reports “Version incompatible - sequential upgrade required”

Solutions:

  1. Check CHANGELOG for upgrade path requirements
  2. Perform intermediate upgrade first
  3. Open an issue with the logs and upgrade path details if the required sequence is unclear

Health Check False Positives

Symptom: External rollback triggers despite cluster appearing healthy

Diagnosis:

# Check actual latency metrics
curl http://node1:9100/metrics | grep request_duration

# Review external rollback reason in logs
journalctl -u archerdb | grep -i rollback

Solutions:

  1. Increase thresholds: --p99-threshold-x10=30
  2. Review baseline latency (may have been unusually low)
  3. Check for external factors affecting latency

CLI Reference

Status Command

archerdb upgrade status --addresses=<addresses> [--metrics-port=<port>] [--format=<text|json>]

Shows current cluster versions, identifies primary, and displays upgrade readiness.

Start Command

archerdb upgrade start --addresses=<addresses> --target-version=<version> \
  --dry-run \
  [--metrics-port=<port>] \
  [--p99-threshold-x10=<value>] \
  [--error-threshold-x10=<value>] \
  [--catchup-timeout=<seconds>] \
  [--format=<text|json>]

Generates a dry-run rolling-upgrade plan to the target version. --dry-run is required in the current runtime surface.

The ArcherDB CLI does not own process restarts, pause/resume state, or rollback actuation. Use your deployment tooling for those mutations and use upgrade status plus upgrade start --dry-run for inspection and planning.


Edit this page

Troubleshooting Guide

This guide covers common issues encountered when operating ArcherDB and provides step-by-step resolution procedures.

Quick Diagnosis

Use this table to quickly identify and resolve common issues:

Symptom Likely Cause Quick Fix
503 on /health/ready Not yet synced after start Wait 30s for replica to sync, check logs for progress
503 persists after 60s Failed to join cluster Check network connectivity between replicas
High P99 latency (>100ms) Compaction backlog Increase compaction_threads to 3, check disk I/O
Connection refused Server not running or wrong port Verify systemctl status archerdb, check port binding
“cluster ID mismatch” Client configured for wrong cluster Update client cluster_id to match server
“max clients reached” Connection limit hit Check clients_max for your tier (lite/standard=64, pro=128, enterprise/ultra=256), close idle connections
TTL cleanup removes 0 TTL not configured on events Events must have ttl_seconds > 0 when inserted
Replica lag growing Follower disk too slow Check iostat on lagging replica, upgrade to NVMe
Memory usage growing Entity count exceeds plan Check entity count vs capacity, scale or archive data
View changes frequent Network instability Check packet loss between replicas with ping -c 100
IndexDegraded alert RAM index at capacity Increase ram_index_capacity or scale horizontally
Disk fill prediction Write rate exceeds capacity Enable TTL, archive old data, or expand storage

For alert-specific runbooks, see:

  • Replica Down Runbook - Critical replica failure
  • deploy/prometheus/rules.yaml - All alert definitions

Table of Contents

How to Use This Guide

Quick Resolution

Each issue follows a consistent format:

  • Symptom: What you observe
  • Possible Causes: Likely root causes ranked by frequency
  • Resolution: Step-by-step fix for each cause
  • Prevention: How to avoid this issue in the future

When to Escalate

Escalate to senior support or engineering if:

  • The issue persists after following all resolution steps
  • You observe data corruption or inconsistency
  • Multiple unrelated symptoms appear simultaneously
  • The issue requires cluster-wide downtime to resolve

Log Locations

Component Log Location Format
ArcherDB (systemd) journalctl -u archerdb Structured JSON or text
ArcherDB (manual) stdout/stderr Structured JSON or text
Metrics localhost:9090/metrics Prometheus format
Health localhost:9090/health/detailed JSON

For log format configuration, see --log-format and --log-level options.

Connection Issues

Connection Refused

Symptom: Client receives “connection refused” error when connecting to ArcherDB.

Possible Causes:

  1. Server not running
  2. Wrong port number
  3. Firewall blocking the port
  4. Binding to wrong network interface

Resolution:

  1. Check if server is running:

    systemctl status archerdb
    # Or
    pgrep -f archerdb

    If not running, start it: systemctl start archerdb

  2. Verify listening port:

    ss -tlnp | grep archerdb
    # Or
    netstat -tlnp | grep 3000

    Ensure ArcherDB is listening on the expected port.

  3. Check firewall rules:

    # Linux (iptables)
    iptables -L -n | grep 3000
    
    # Linux (firewalld)
    firewall-cmd --list-ports
    
    # Cloud: Check security groups/firewall rules in console
  4. Verify bind address:

    # Check config or command line for --bind option
    # Default: 0.0.0.0 (all interfaces)

Prevention: Use monitoring to alert on server unavailability. Configure health checks in load balancers.


Connection Timeout

Symptom: Client connections hang and eventually timeout.

Possible Causes:

  1. Network routing issues
  2. DNS resolution failure
  3. Load balancer misconfiguration
  4. Server overloaded

Resolution:

  1. Test network connectivity:

    # From client machine
    ping node1
    telnet node1 3000
    nc -zv node1 3000
  2. Verify DNS resolution:

    nslookup node1
    dig node1
  3. Check load balancer health:

    # Verify backend health in LB dashboard
    # Check LB logs for connection errors
  4. Check server load:

    curl -s localhost:9090/metrics | grep archerdb_connections_active
    curl -s localhost:9090/metrics | grep process_open_fds

Prevention: Implement connection timeouts in clients. Monitor connection counts and latency.


Cluster ID Mismatch

Symptom: Client receives “cluster ID mismatch” error.

Possible Causes:

  1. Client configured with wrong cluster ID
  2. Connecting to wrong cluster
  3. Data file from different cluster

Resolution:

  1. Check server cluster ID:

    ./archerdb info /data/archerdb.db | grep cluster
    # Or check startup logs for "cluster_id"
  2. Update client configuration:

    # Ensure cluster_id matches server
    client = ArcherDBClient(
        addresses=["node1:3000"],
        cluster_id=12345  # Must match server
    )
  3. If data file is wrong: Restore from external snapshot/archive or re-format with correct cluster ID.

Prevention: Store cluster IDs in configuration management. Use separate DNS names per cluster.


Gateway TLS Handshake Failed

Symptom: Connection fails with TLS/SSL handshake error at gateway/proxy boundary.

Possible Causes:

  1. Certificate expired
  2. Hostname mismatch
  3. CA certificate not trusted
  4. Protocol/cipher mismatch in gateway or service mesh

Resolution:

  1. Check certificate expiry in gateway/proxy:

    openssl x509 -in /path/to/cert.pem -noout -dates

    If expired, rotate certificates (see Certificate Rotation).

  2. Verify hostname/SAN matches exposed endpoint:

    openssl x509 -in /path/to/cert.pem -noout -text | grep -A1 "Subject Alternative Name"
  3. Test TLS endpoint directly:

    openssl s_client -connect node1:3000 -CAfile /path/to/ca.pem
  4. Check TLS version/cipher compatibility: Ensure client and gateway/proxy settings are aligned.

Prevention: Monitor certificate expiry dates. Automate rotation in gateway/mesh control plane. Keep TLS policy managed centrally.

Performance Issues

High Latency (P99 > 100ms)

Symptom: Request latency exceeds acceptable thresholds.

Possible Causes:

  1. Disk I/O saturation
  2. Compaction running
  3. Large batch sizes causing queuing
  4. Insufficient memory for block cache

Resolution:

  1. Check disk I/O:

    iostat -x 1 5
    # Look for %util > 80% or high await times

    If disk saturated, consider faster storage (NVMe).

  2. Check for active compaction:

    curl -s localhost:9090/metrics | grep archerdb_compaction_active
    curl -s localhost:9090/metrics | grep archerdb_compaction_write_amp

    Compaction is normal but can cause latency spikes. See LSM Tuning.

  3. Reduce batch sizes: Large batches (>5000 events) can cause queuing delays. Optimal range: 500-2000.

  4. Check memory pressure:

    curl -s localhost:9090/metrics | grep process_resident_memory_bytes
    free -h

    If memory constrained, increase RAM or reduce entity count.

Prevention: Monitor P99 latency trends. Set up alerts at 50ms (warning) and 100ms (critical).


Low Throughput

Symptom: Insert or query rate lower than expected.

Possible Causes:

  1. Connection pool too small
  2. Batch sizes too small
  3. Client-side bottleneck
  4. Network bandwidth limit

Resolution:

  1. Increase connection pool:

    client = ArcherDBClient(
        addresses=["node1:3000"],
        pool_size=10  # Increase from default
    )
  2. Increase batch sizes:

    batch = client.create_batch()
    # Add 1000-5000 events per batch instead of small batches
    for event in events:
        batch.add(event)
    batch.commit()
  3. Profile client application: Ensure client isn’t CPU-bound processing results.

  4. Check network throughput:

    iperf3 -c node1 -p 5201

Prevention: Benchmark during capacity planning. Monitor throughput metrics over time.


High Memory Usage

Symptom: Memory usage approaching limits, potential OOM.

Possible Causes:

  1. RAM index grown beyond capacity plan
  2. Memory leak (rare)
  3. Large query result sets in memory

Resolution:

  1. Check entity count vs. capacity:

    curl -s localhost:9090/metrics | grep archerdb_entities_total
    # Compare to capacity plan

    See Capacity Planning for sizing.

  2. Check index load factor:

    curl -s localhost:9090/metrics | grep archerdb_index_load_factor
    # Should be < 0.7 for optimal performance
  3. Restart if memory keeps growing (potential leak):

    systemctl restart archerdb
    # Monitor if issue recurs

Prevention: Set resource limits. Monitor memory usage with alerts at 70% and 85%.


Disk Usage Growing

Symptom: Disk usage continuously increasing.

Possible Causes:

  1. TTL not configured
  2. Compaction falling behind
  3. High update rate creating versions

Resolution:

  1. Check if TTL is configured:

    ./archerdb info /data/archerdb.db | grep ttl

    Consider enabling TTL for automatic cleanup.

  2. Check compaction status:

    curl -s localhost:9090/metrics | grep archerdb_lsm_levels
    curl -s localhost:9090/metrics | grep archerdb_compaction

    If levels accumulating, compaction may be behind.

  3. Manual compaction:

    ./archerdb compact /data/archerdb.db

Prevention: Configure TTL appropriate for use case. Monitor disk usage trends.

Cluster Issues

Cluster Won’t Form Quorum

Symptom: Replicas don’t elect a primary; cluster unavailable.

Possible Causes:

  1. Network partition between replicas
  2. Clock skew too large
  3. Data corruption preventing startup
  4. Wrong cluster configuration

Resolution:

  1. Test network connectivity between replicas:

    # From each node, test others
    for node in node1 node2 node3; do
      echo "Testing $node"
      nc -zv $node 3000
    done
  2. Check clock synchronization:

    chronyc tracking
    # Or
    timedatectl status
    # Clock skew should be < 100ms

    If skewed, fix NTP: systemctl restart chronyd

  3. Verify data file integrity:

    ./archerdb verify /data/archerdb.db

    If corrupted, see Disaster Recovery.

  4. Check configuration consistency: Ensure all replicas use same --addresses list and --cluster ID.

Prevention: Monitor clock skew and network connectivity. Use consistent configuration management.


Frequent View Changes

Symptom: archerdb_view_changes_total incrementing frequently.

Possible Causes:

  1. Network instability
  2. Disk I/O latency causing heartbeat timeouts
  3. Resource exhaustion (CPU/memory)

Resolution:

  1. Check network stability:

    # Test packet loss between replicas
    ping -c 100 node2 | grep "packet loss"
  2. Check disk latency:

    iostat -x 1 5
    # Check await column - should be < 10ms for SSD
  3. Check resource usage:

    top -p $(pgrep archerdb)
    curl -s localhost:9090/metrics | grep process_

Prevention: Use stable network infrastructure. Monitor view change rate. Alert on > 3 changes per 5 minutes.


Replica Falling Behind

Symptom: One replica’s archerdb_replication_lag_ms consistently high.

Possible Causes:

  1. Slow disk on follower
  2. Network congestion to follower
  3. Follower under-resourced

Resolution:

  1. Check disk performance on lagging replica:

    ssh lagging-node "iostat -x 1 5"
  2. Check network path:

    iperf3 -c lagging-node -p 5201
    mtr lagging-node
  3. Compare resources: Ensure lagging replica has same CPU/RAM/disk spec as others.

Prevention: Use homogeneous hardware. Monitor replication lag per replica.


Split Brain Suspected

Symptom: Concern about split brain after network partition.

Possible Causes: This is not possible with ArcherDB’s Viewstamped Replication (VSR) protocol.

Explanation:

  • VSR requires a majority (2 of 3, 3 of 5) to commit any operation
  • During a partition, only one partition can have a majority
  • The minority partition cannot accept writes (returns cluster_unavailable)
  • When the partition heals, the minority automatically catches up

Resolution:

  1. Verify cluster state:

    # Check each replica's view number
    for node in node1 node2 node3; do
      echo -n "$node view: "
      ssh $node "curl -s localhost:9090/metrics | grep archerdb_view_number"
    done

    All healthy replicas should have the same view number.

  2. If partitioned now, identify majority: The partition accepting writes has quorum. Wait for network to heal.

Prevention: Use reliable network infrastructure. Monitor view numbers across replicas.

Query Issues

Radius Query Returns No Results

Symptom: Radius query returns empty results when data expected.

Possible Causes:

  1. Coordinate encoding mismatch (degrees vs. nanodegrees)
  2. Query area actually empty
  3. Wrong group ID filter
  4. Data not yet replicated

Resolution:

  1. Verify coordinate encoding:

    # ArcherDB uses nanodegrees internally
    # SDK should handle conversion, but verify:
    lat = 37.7749  # degrees
    lat_nano = 37774900000  # nanodegrees (lat * 1e9)
  2. Check with broader query:

    # Expand radius to verify data exists
    results = client.query_radius(
        center_lat=37.7749,
        center_lon=-122.4194,
        radius_m=100000  # 100km to find any nearby data
    )
  3. Verify group ID:

    # Try without group filter
    results = client.query_radius(..., group_id=None)
  4. Check if data committed: Recent inserts may not be queryable until committed (milliseconds).

Prevention: Add unit tests for coordinate encoding. Log query parameters.


Polygon Query Rejects Input

Symptom: Polygon query returns “invalid polygon” error.

Possible Causes:

  1. Wrong winding order (exterior must be counter-clockwise)
  2. Self-intersecting polygon
  3. Too few vertices (minimum 4 for closed ring)
  4. Holes with wrong winding order (must be clockwise)

Resolution:

  1. Check winding order:

    # Exterior ring: counter-clockwise
    # Holes: clockwise
    exterior = [
        (-122.4, 37.7),   # Start
        (-122.3, 37.7),   # Go counter-clockwise
        (-122.3, 37.8),
        (-122.4, 37.8),
        (-122.4, 37.7),   # Close the ring
    ]
  2. Check for self-intersection: Use a GIS tool or library to validate the polygon.

  3. Verify ring closure: First and last vertex must be identical.

Prevention: Validate polygons before querying. Use well-known-text (WKT) validation libraries.


Results Seem Incomplete

Symptom: Query returns fewer results than expected.

Possible Causes:

  1. Result limit reached (default: 1000)
  2. Pagination required
  3. Filter excluding data (group_id, time range)

Resolution:

  1. Check for pagination:

    results = client.query_radius(...)
    all_events = results.events
    
    while results.has_more:
        results = client.query_radius(..., cursor=results.cursor)
        all_events.extend(results.events)
    
    print(f"Total: {len(all_events)}")
  2. Increase limit if needed:

    results = client.query_radius(..., limit=10000)  # Max: 10000
  3. Remove filters to test: Try query without group_id or time constraints.

Prevention: Always handle pagination in client code. Log result counts.

Replication Issues

S3 Log Shipping Failing

Symptom: S3 log-shipping uploads failing; archerdb_replication_state shows degraded.

Possible Causes:

  1. Invalid credentials
  2. Bucket permissions
  3. Network connectivity to S3
  4. S3 service outage

Resolution:

  1. Check credentials:

    # Verify AWS credentials
    aws sts get-caller-identity
    
    # Test S3 access
    aws s3 ls s3://your-bucket/
  2. Check bucket policy: Ensure IAM role/user has s3:PutObject, s3:GetObject, s3:ListBucket.

  3. Test network connectivity:

    curl -I https://s3.amazonaws.com
    # Or your regional endpoint
  4. Check spillover directory:

    ls -la /data/spillover/
    # Files here indicate S3 writes are queued

Prevention: Use IAM roles (not keys) on EC2. Monitor replication state metric. Set up S3 bucket notifications.


Replication Lag High

Symptom: archerdb_replication_lag_seconds consistently elevated.

Possible Causes:

  1. S3 throttling
  2. Network bandwidth limitation
  3. High write volume

Resolution:

  1. Check for S3 throttling:

    # Check S3 metrics in CloudWatch for 503 errors
    aws cloudwatch get-metric-statistics \
      --namespace AWS/S3 \
      --metric-name 5xxErrors \
      --dimensions Name=BucketName,Value=your-bucket
  2. Check upload bandwidth:

    curl -s localhost:9090/metrics | grep archerdb_replication_bytes
  3. Consider S3 Transfer Acceleration or multi-region setup.

Prevention: Use appropriate S3 tier. Monitor replication lag with alerts at 30s and 2min.


Spillover Files Growing

Symptom: Files accumulating in spillover directory.

Possible Causes:

  1. S3 outage or prolonged failure
  2. Credentials expired
  3. Network partition to S3

Resolution:

  1. Check S3 connectivity:

    aws s3 ls s3://your-bucket/
  2. Check credential expiry: For IAM roles, ensure instance profile is attached.

  3. Monitor spillover directory:

    du -sh /data/spillover/
    ls -lt /data/spillover/ | head
  4. When S3 recovers: Spillover files are automatically uploaded in order. Monitor until directory empties.

Prevention: Alert on spillover directory size. Use multiple S3 regions for redundancy.

Data Protection Issues

ArcherDB expects encryption-at-rest and key operations to be managed by storage/cloud infrastructure.

Storage Encryption Policy Drift

Symptom: Security scans report unencrypted volumes/snapshots.

Possible Causes:

  1. New volume created outside policy
  2. Snapshot replication policy disabled
  3. Wrong storage class or account defaults

Resolution:

  1. Verify encryption settings on active data volumes and snapshots.
  2. Recreate non-compliant resources with enforced encryption policy.
  3. Re-run restore drill from compliant snapshot set.

External Key Service Issues

Symptom: Platform tooling reports KMS/key policy failures.

Possible Causes:

  1. KMS connectivity or endpoint policy issue
  2. IAM permissions drift
  3. Key disabled/deleted

Resolution:

  1. Validate key accessibility with platform tooling.
  2. Restore least-privilege IAM/key policies from IaC baseline.
  3. Ensure restore pipeline can access required keys before recovery exercises.

Diagnostic Commands

Health Checks

# Quick health check
curl -s localhost:9090/health/live
# Returns: {"status":"ok"}

# Readiness check
curl -s localhost:9090/health/ready
# Returns: {"status":"ready"} or {"status":"not_ready","reason":"..."}

# Detailed health with component status
curl -s localhost:9090/health/detailed | jq .
# Returns component-level health: replica, memory, storage, replication

Metrics Inspection

# All metrics
curl -s localhost:9090/metrics

# Specific metric patterns
curl -s localhost:9090/metrics | grep archerdb_request_duration
curl -s localhost:9090/metrics | grep archerdb_replication
curl -s localhost:9090/metrics | grep archerdb_compaction
curl -s localhost:9090/metrics | grep process_

# Current connections
curl -s localhost:9090/metrics | grep archerdb_connections

# Entity count
curl -s localhost:9090/metrics | grep archerdb_entities_total

Log Analysis

# Recent errors
journalctl -u archerdb --since "1 hour ago" | grep -i error

# View changes
journalctl -u archerdb | grep -i "view change"

# Replication events
journalctl -u archerdb | grep -i "replication"

# Connection events
journalctl -u archerdb | grep -i "connection"

# JSON log parsing (if using JSON format)
journalctl -u archerdb -o cat | jq 'select(.level == "error")'

Data File Verification

# Verify data file integrity
./archerdb verify /data/archerdb.db

# Show data file info
./archerdb info /data/archerdb.db

# Check disk usage
du -sh /data/archerdb.db
df -h /data

Cluster Status

# Cluster health
./archerdb status --addresses=node1:3000,node2:3000,node3:3000

# Check which replica is primary
curl -s localhost:9090/metrics | grep archerdb_replica_role
# 1 = primary, 0 = follower

# View number (should match across replicas)
curl -s localhost:9090/metrics | grep archerdb_view_number

Getting Help

When the troubleshooting steps above don’t resolve your issue, follow these steps to get help.

Collecting Diagnostic Information

Before reaching out, collect the following:

# 1. System information
uname -a
cat /etc/os-release

# 2. ArcherDB version
./archerdb --version

# 3. Configuration (redact sensitive values)
cat /etc/archerdb/config.yaml | grep -v password | grep -v key

# 4. Recent logs (last 1000 lines)
journalctl -u archerdb --since "30 minutes ago" | tail -1000 > archerdb-logs.txt

# 5. Metrics snapshot
curl -s localhost:9090/metrics > archerdb-metrics.txt

# 6. Health check details
curl -s localhost:9090/health/detailed | jq . > archerdb-health.json

# 7. Cluster state (if multi-node)
for i in 0 1 2; do
  echo "=== Node $i ===" >> cluster-state.txt
  curl -s node$i:9090/metrics | grep -E "archerdb_(view|replica|replication)" >> cluster-state.txt
done

What to Include in Bug Reports

When opening a GitHub issue, include:

  1. Environment: OS, ArcherDB version, hardware specs (CPU, RAM, disk type)
  2. Configuration: Cluster size, relevant config settings
  3. Reproduction steps: Exact sequence to reproduce the issue
  4. Expected behavior: What should happen
  5. Actual behavior: What actually happens
  6. Logs: Relevant log snippets (redact sensitive data)
  7. Metrics: Relevant metric values at time of issue

Support Channels

Channel Best For Response Time
GitHub Issues Bug reports, feature requests Community: 1-7 days
GitHub Discussions Questions, best practices Community: 1-3 days
Documentation Self-service troubleshooting Immediate

Emergency Procedures

For production emergencies:

  1. Data loss suspected: Stop writes, capture external snapshot, preserve logs
  2. Cluster unavailable: Check quorum (need 2/3 or 3/5 replicas)
  3. Security incident: Isolate affected nodes, preserve evidence, rotate credentials

See Disaster Recovery for emergency procedures.

Edit this page

ArcherDB Error Codes Reference

This document provides a complete reference for all ArcherDB error codes.

Error Code Ranges

Range Category Description
0 Success Operation succeeded
1-99 Protocol Message format, checksums, version
100-199 Validation Invalid inputs, constraint violations
200-299 State Entity/cluster state errors
300-399 Resource Limits exceeded, capacity constraints
400-499 Security External security-boundary policy and access controls
500-599 Internal Bugs (should not occur in production)

Retry Semantics

Errors are classified into three categories:

  • Retryable: Transient errors that may succeed on retry (e.g., leader election, network issues)
  • Client Error: Invalid request that will always fail (fix the request, don’t retry)
  • Fatal: Server-side bugs (open an issue with logs and reproduction details)

Distributed Error Codes

Multi-Region Errors (213-218)

These errors occur in multi-region deployments with async replication.

Code Name Message Retryable
213 FOLLOWER_READ_ONLY Write operation rejected: follower regions are read-only No
214 STALE_FOLLOWER Follower data exceeds maximum staleness threshold Yes
215 PRIMARY_UNREACHABLE Cannot connect to primary region Yes
216 REPLICATION_TIMEOUT Cross-region replication timeout Yes
217 CONFLICT_DETECTED Write conflict detected in active-active replication No
218 GEO_SHARD_MISMATCH Entity geo-shard does not match target region No

Usage Notes:

  • Code 213: Writes must go to the primary region. SDKs automatically route writes to primary.
  • Code 214: The follower hasn’t caught up with replication. Wait and retry, or use a fresher replica.
  • Code 215: The primary region is down. Wait for failover or recovery.
  • Code 216: Cross-region replication is slow. Retry with backoff.
  • Code 217: Concurrent writes to the same entity detected in active-active replication. Application needs conflict resolution.
  • Code 218: Entity’s geo-shard doesn’t match the region handling the request. Check shard routing configuration.

Sharding Errors (220-224)

These errors occur in sharded cluster deployments.

Code Name Message Retryable
220 NOT_SHARD_LEADER This node is not the leader for target shard Yes
221 SHARD_UNAVAILABLE Target shard has no available replicas Yes
222 RESHARDING_IN_PROGRESS Cluster is currently resharding Yes
223 INVALID_SHARD_COUNT Target shard count is invalid No
224 SHARD_MIGRATION_FAILED Data migration to new shard failed No

Usage Notes:

  • Code 220: SDKs automatically refresh topology and retry. No application action needed.
  • Code 221: Wait for shard recovery. The cluster may be experiencing failures.
  • Code 222: Wait for resharding to complete. Operations will succeed after.
  • Code 223: The requested shard count is not valid (e.g., must be power of 2).
  • Code 224: A resharding operation failed. Check cluster health.

Security Boundary Errors (410-414, reserved/legacy)

These codes are reserved for deployments that layer external security controls around ArcherDB.

Code Name Message Retryable
410 ENCRYPTION_KEY_UNAVAILABLE External key service unavailable Yes
411 DECRYPTION_FAILED External data-protection validation failed No
412 ENCRYPTION_NOT_ENABLED External encryption policy not satisfied No
413 KEY_ROTATION_IN_PROGRESS External key rotation in progress Yes
414 UNSUPPORTED_ENCRYPTION_VERSION Unsupported external data-protection format/version No

Usage Notes:

  • Code 410: Check external key management service availability and IAM/policy bindings.
  • Code 411: Validate storage snapshot integrity and external decryption path.
  • Code 412: Verify infrastructure policy requires encrypted storage/transport for this route.
  • Code 413: Retry after external key rotation completes.
  • Code 414: Align external tooling format/version with deployment standards.

SDK Error Handling

Python

from archerdb import (
    MultiRegionError,
    ShardingError,
    MultiRegionException,
    ShardingException,
    is_retryable,
)

try:
    result = client.query_radius(lat, lon, radius)
except ShardingException as e:
    if e.error == ShardingError.RESHARDING_IN_PROGRESS:
        # Wait and retry - cluster is resharding
        time.sleep(5)
        result = client.query_radius(lat, lon, radius)
    elif is_retryable(e.code):
        # Generic retry logic
        result = retry_with_backoff(lambda: client.query_radius(lat, lon, radius))
    else:
        raise  # Non-retryable error

Java

import com.archerdb.geo.ShardingError;
import com.archerdb.geo.ArcherDBException;

try {
    QueryResult result = client.queryRadius(lat, lon, radius);
} catch (ArcherDBException e) {
    ShardingError shardError = ShardingError.fromCode(e.getErrorCode());
    if (shardError != null && shardError.isRetryable()) {
        // Retry with backoff
    }
}

Go

import "github.com/ArcherDB-io/archerdb/src/clients/go/pkg/errors"

result, err := client.QueryRadius(lat, lon, radius)
if err != nil {
    if archerErr, ok := err.(*errors.ArcherDBError); ok {
        if errors.IsShardingError(int(archerErr.Code)) {
            if errors.IsRetryable(int(archerErr.Code)) {
                // Retry with backoff
            }
        }
    }
}

Node.js/TypeScript

import {
    ShardingError,
    ShardingException,
    isShardingError,
    isRetryable,
} from 'archerdb';

try {
    const result = await client.queryRadius(lat, lon, radius);
} catch (e) {
    if (e instanceof ShardingException) {
        if (e.error === ShardingError.RESHARDING_IN_PROGRESS) {
            // Wait and retry
            await sleep(5000);
            result = await client.queryRadius(lat, lon, radius);
        }
    }
}

Troubleshooting Guide

Multi-Region Issues

Symptom Likely Cause Solution
All writes fail with 213 Connected to follower Configure SDK with primary region
Reads return stale data Replication lag Check read_staleness_ns header
215 errors during failover Primary down Wait for new primary election

Sharding Issues

Symptom Likely Cause Solution
Frequent 220 errors Topology cache stale Reduce topology_refresh_interval
221 errors cluster-wide Shard failure Check cluster health, may need recovery
Long 222 wait times Large resharding Monitor resharding progress

Security Boundary Issues (410-414)

Symptom Likely Cause Solution
410 errors at startup External key service unreachable Check key-service connectivity and IAM/policy
411 errors on read External protection/integrity failure Restore from validated external snapshot
413 during rotation External key rotation window Wait for completion and retry
Edit this page

Alert: ArcherDBReplicaDown

Quick Reference

  • Severity: critical
  • Metric: up{job="archerdb"}
  • Threshold: == 0 (replica unreachable)
  • Time to Respond: Immediate (affects quorum)

What This Alert Means

A replica is not responding to health checks. If multiple replicas go down simultaneously, the cluster may lose quorum and become unavailable for writes.

Immediate Actions

  1. [ ] Check if the pod/process is running
  2. [ ] Verify network connectivity to the replica
  3. [ ] Check for resource exhaustion (OOM, disk full)
  4. [ ] Verify remaining replicas have quorum (2 of 3 minimum)

Investigation

Common Causes

  • Process crash: OOM kill, unhandled error, or bug
  • Node failure: Hardware issue, kernel panic, or cloud provider incident
  • Network partition: Replica is running but unreachable from Prometheus
  • Resource exhaustion: Out of memory, disk full, or file descriptor limit

Diagnostic Commands

# Check pod status (Kubernetes)
kubectl get pods -n archerdb -l app=archerdb

# Check pod events
kubectl describe pod archerdb-N -n archerdb | tail -20

# Check if process is running (bare metal)
systemctl status archerdb
pgrep -f archerdb

# Check recent logs
kubectl logs archerdb-N -n archerdb --tail=100
# Or
journalctl -u archerdb --since "10 minutes ago"

# Check resource usage
kubectl top pod archerdb-N -n archerdb
# Or
free -h && df -h /data

# Test network from another replica
kubectl exec archerdb-0 -n archerdb -- nc -zv archerdb-N.archerdb-headless.archerdb.svc.cluster.local 3000

Resolution

Process Crashed

  1. Check logs for crash reason:

    kubectl logs archerdb-N -n archerdb --previous
  2. If OOM killed, increase memory limits:

    resources:
      limits:
        memory: "8Gi"  # Increase from default
  3. Restart the pod:

    kubectl delete pod archerdb-N -n archerdb
    # StatefulSet will recreate it

Node Failure

  1. Check node status:

    kubectl get nodes
    kubectl describe node <node-name>
  2. If node is unhealthy, pod will be rescheduled automatically (may take 5+ minutes).

  3. For faster recovery, delete the pod to trigger immediate reschedule:

    kubectl delete pod archerdb-N -n archerdb --force --grace-period=0

Network Partition

  1. Verify network policies allow inter-pod communication:

    kubectl get networkpolicy -n archerdb
  2. Check DNS resolution:

    kubectl exec archerdb-0 -n archerdb -- nslookup archerdb-N.archerdb-headless.archerdb.svc.cluster.local
  3. Test port connectivity:

    kubectl exec archerdb-0 -n archerdb -- nc -zv archerdb-N.archerdb-headless.archerdb.svc.cluster.local 3000

Resource Exhaustion

  1. Out of memory: Increase memory limits or reduce entity count
  2. Disk full: See Disk Capacity Runbook
  3. File descriptors: Check ulimits and increase if needed

Prevention

  • PodDisruptionBudget: Configure minAvailable: 2 to prevent simultaneous evictions
  • Resource limits: Set appropriate memory and CPU limits based on workload
  • Anti-affinity: Spread replicas across nodes/zones
  • Monitoring: Alert on memory usage > 80% before OOM
  • Node health: Monitor node conditions and drain unhealthy nodes proactively

Post-Recovery Verification

After the replica recovers:

# Verify replica is catching up
kubectl exec archerdb-N -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_replication_lag

# Verify view number matches other replicas
for i in 0 1 2; do
  echo -n "archerdb-$i: "
  kubectl exec archerdb-$i -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_view_number
done

# Verify cluster health
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/health/detailed
Edit this page

Alert: ArcherDBViewChangeFrequent

Quick Reference

  • Severity: warning
  • Metric: archerdb_view_changes_total
  • Threshold: increase(...[5m]) > 3 (more than 3 view changes in 5 minutes)
  • Time to Respond: Within 15 minutes

What This Alert Means

Too many leader elections (view changes) are occurring, indicating cluster instability. While the cluster remains available, frequent view changes cause brief write pauses and may indicate an underlying issue that could lead to unavailability.

Immediate Actions

  1. [ ] Check all replica health status
  2. [ ] Identify which replica(s) are triggering view changes
  3. [ ] Check for network issues between replicas
  4. [ ] Review resource utilization on all replicas

Investigation

Common Causes

  • Network instability: Packet loss or high latency between replicas
  • Resource exhaustion: CPU saturation or disk I/O delays causing heartbeat timeouts
  • Clock skew: Significant time drift between replicas
  • Failing replica: One replica repeatedly crashing or hanging

Diagnostic Commands

# Check view change history per replica
for i in 0 1 2; do
  echo "=== archerdb-$i ==="
  kubectl exec archerdb-$i -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_view_changes_total
done

# Check which replica is currently primary
for i in 0 1 2; do
  echo -n "archerdb-$i role: "
  kubectl exec archerdb-$i -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_replica_role
done
# 1 = primary, 0 = follower

# Check network latency between replicas
kubectl exec archerdb-0 -n archerdb -- ping -c 10 archerdb-1.archerdb-headless.archerdb.svc.cluster.local

# Check for packet loss
kubectl exec archerdb-0 -n archerdb -- ping -c 100 archerdb-1.archerdb-headless.archerdb.svc.cluster.local | tail -3

# Check CPU usage
kubectl top pod -n archerdb -l app=archerdb

# Check disk I/O
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5

Log Analysis

# Search for view change events in logs
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "view change"

# Check for timeout events
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "timeout"

# Check for heartbeat failures
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "heartbeat"

Resolution

Network Instability

  1. Identify network issues:

    # Test sustained connectivity
    kubectl exec archerdb-0 -n archerdb -- mtr -c 100 --report archerdb-1.archerdb-headless.archerdb.svc.cluster.local
  2. Check for network policy issues:

    kubectl get networkpolicy -n archerdb -o yaml
  3. For cloud deployments: Check cloud provider status and network logs.

CPU/Disk Saturation

  1. Check resource usage:

    kubectl top pod -n archerdb
    kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
  2. If CPU saturated: Increase CPU limits or investigate high-CPU operations.

  3. If disk slow: Check for compaction backlog or storage issues. See Compaction Backlog.

Clock Skew

  1. Check time sync status:

    kubectl exec archerdb-0 -n archerdb -- chronyc tracking
    # Or
    kubectl exec archerdb-0 -n archerdb -- timedatectl status
  2. If clock skewed > 100ms: Fix NTP configuration on affected nodes.

Failing Replica

  1. Identify which replica has issues:

    # Check view change counts - higher count indicates the problem replica
    for i in 0 1 2; do
      echo -n "archerdb-$i view_changes: "
      kubectl exec archerdb-$i -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_view_changes_total
    done
  2. Check that replica’s logs:

    kubectl logs archerdb-N -n archerdb --since=30m | grep -E "(error|warning|panic)" -i
  3. Restart the problematic replica:

    kubectl delete pod archerdb-N -n archerdb

Prevention

  • Network reliability: Use reliable network infrastructure, consider dedicated network for cluster traffic
  • Resource headroom: Keep CPU < 70% and memory < 80% under normal load
  • Monitoring: Alert on individual replica metrics, not just cluster aggregates
  • Time sync: Ensure NTP is properly configured on all nodes
  • Pod anti-affinity: Spread replicas across different nodes to isolate failures
Edit this page

Alert: ArcherDBIndexDegraded

Quick Reference

  • Severity: critical
  • Metric: archerdb_index_probe_limit_hits_total
  • Threshold: > 0 (any probe limit hit)
  • Time to Respond: Within 1 hour (impacts query performance)

What This Alert Means

The RAM index is operating in degraded mode due to hash collisions. When the index is too small for the entity count, lookups require additional probing which significantly slows queries. This alert indicates the ram_index_capacity setting needs to be increased.

Immediate Actions

  1. [ ] Check current entity count vs index capacity
  2. [ ] Assess query latency impact
  3. [ ] Plan capacity increase (requires restart)
  4. [ ] Schedule maintenance window if immediate action needed

Investigation

Common Causes

  • Entity growth: Data volume exceeded capacity planning assumptions
  • Under-provisioned: Initial ram_index_capacity was set too low
  • Hot spots: Uneven hash distribution causing localized collisions

Diagnostic Commands

# Check entity count vs capacity
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep -E "(archerdb_entities_total|archerdb_index)"

# Check index load factor (should be < 0.5 for optimal performance)
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_load_factor

# Check how many probe limit hits occurred
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_probe_limit_hits_total

# Check current capacity configuration
kubectl exec archerdb-0 -n archerdb -- ./archerdb info /data/archerdb.db | grep -i index

Impact Assessment

# Check query latency - degraded index causes P99 spikes
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_read_latency_seconds

# Check for query timeouts
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "timeout"

Resolution

Increase Index Capacity

The index capacity must be configured at startup. A rolling restart is required.

  1. Calculate required capacity:

    Current entities: N
    Target capacity: N * 2 (for 50% load factor)
    Recommended minimum: 500,000 (Phase 5 optimization)
  2. Update configuration:

    For Helm deployment:

    # values.yaml
    config:
      ram_index_capacity: 1000000  # Increase to 1M

    For bare metal:

    # Update startup script or systemd unit
    ./archerdb start --ram-index-capacity=1000000 ...
  3. Perform rolling restart:

    # Kubernetes - update StatefulSet
    kubectl rollout restart statefulset/archerdb -n archerdb
    
    # Monitor rollout
    kubectl rollout status statefulset/archerdb -n archerdb

    For bare metal, see Rolling Restart.

Sizing Guidelines

Entity Count Recommended Capacity Load Factor
< 100K 250,000 ~40%
100K - 250K 500,000 ~50%
250K - 500K 1,000,000 ~50%
500K - 1M 2,000,000 ~50%
> 1M entity_count * 2 ~50%

Note: Memory usage scales with capacity. Each additional 100K capacity adds ~1MB RAM.

Prevention

  • Capacity planning: Use Capacity Planning Guide to set appropriate capacity
  • Monitoring: Alert when load factor > 0.6 (warning) and > 0.7 (critical)
  • Growth forecasting: Track entity growth rate and plan capacity increases proactively
  • Headroom: Always maintain 50% headroom above current entity count

Verification

After increasing capacity:

# Verify new capacity is active
kubectl exec archerdb-0 -n archerdb -- ./archerdb info /data/archerdb.db | grep -i index

# Verify load factor is healthy
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_load_factor
# Should be < 0.5

# Verify no more probe limit hits
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_probe_limit_hits_total
# Counter should stop increasing

# Verify query latency improved
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_read_latency_seconds
Edit this page

Alert: ArcherDBReadLatencyP99Warning / ArcherDBReadLatencyP99Critical

Quick Reference

  • Severity: warning (P99 > 25ms), critical (P99 > 100ms)
  • Metric: archerdb_read_latency_seconds
  • Threshold: Warning: 25ms (25x baseline), Critical: 100ms (100x baseline)
  • Time to Respond: Warning: 1 hour, Critical: 15 minutes

What This Alert Means

Read queries are taking longer than acceptable thresholds. Baseline read latency is approximately 1ms, so these alerts indicate significant degradation:

  • Warning (25ms): 25x baseline - noticeable impact on application performance
  • Critical (100ms): 100x baseline - severe degradation requiring immediate investigation

Immediate Actions

  1. [ ] Check for active compaction
  2. [ ] Verify disk I/O is not saturated
  3. [ ] Check for index degradation (probe limit hits)
  4. [ ] Review recent query patterns

Investigation

Common Causes

  • Compaction activity: LSM compaction causes I/O contention
  • Disk saturation: High write load saturating disk bandwidth
  • Index degradation: RAM index exceeding capacity (see Index Degraded)
  • Large result sets: Queries returning excessive data
  • Cache misses: Block cache not effective for workload

Diagnostic Commands

# Check current latency percentiles
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_read_latency_seconds

# Check for active compaction
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction

# Check disk I/O
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
# Look for %util > 80% or high await times

# Check cache hit rate
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_cache

# Check index health
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index

# Check query rate
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep 'archerdb_operations_total{.*query'

Log Analysis

# Look for slow query warnings
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "slow query"

# Check for compaction events
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "compaction"

Resolution

Compaction-Induced Latency

  1. Verify compaction is the cause:

    kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_active
    # Value of 1 indicates active compaction
  2. Compaction is normal operation. If frequent, tune compaction settings:

    # values.yaml - Phase 5 optimized defaults
    config:
      lsm_l0_compaction_trigger: 8  # Delay compaction start
      lsm_compaction_threads: 3     # More parallel threads
  3. See LSM Tuning for detailed compaction tuning.

Disk Saturation

  1. Check disk utilization:

    kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
    # %util > 80% indicates saturation
  2. Resolution options:

    • Reduce write rate if possible
    • Upgrade to faster storage (NVMe recommended)
    • Increase compaction threads to complete faster

Index Degradation

  1. Check for probe limit hits:

    kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_probe_limit_hits_total
  2. If counter is increasing, see Index Degraded Runbook.

Large Result Sets

  1. Review query patterns:

    • Check if radius queries are using very large radii
    • Check if polygon queries cover large areas
    • Check if limits are not being used
  2. Add limits to queries:

    # Limit result set size
    results = client.query_radius(
        center_lat=37.7749,
        center_lon=-122.4194,
        radius_m=1000,
        limit=100  # Add reasonable limit
    )

Cache Optimization

  1. Check cache effectiveness:

    kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_cache_hit_ratio
  2. If hit ratio < 50%, consider:

    • Increasing block cache size
    • Reviewing query patterns for spatial locality
    • Increasing S2 covering cache (spatial queries)

Prevention

  • Capacity planning: Size storage for peak write rates with headroom for compaction
  • Index sizing: Maintain RAM index at < 50% load factor
  • Query optimization: Use appropriate limits and narrow spatial queries
  • Monitoring: Alert on latency trends, not just thresholds
  • Storage tier: Use NVMe for production workloads

Verification

After resolution:

# Verify latency improved
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_read_latency_seconds

# Check P99 is below threshold
# histogram_quantile(0.99, ...) should be < 0.025 (25ms)

# Monitor for 15 minutes to ensure stability
watch -n 30 'kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_read_latency_seconds | grep quantile=\"0.99\"'
Edit this page

Alert: ArcherDBWriteLatencyP99Warning / ArcherDBWriteLatencyP99Critical / ArcherDBHighLatency

Quick Reference

  • Severity: warning (P99 > 25ms), critical (P99 > 100ms)
  • Metric: archerdb_write_latency_seconds / archerdb_request_duration_seconds
  • Threshold: Warning: 25ms, Critical: 100ms
  • Time to Respond: Warning: 1 hour, Critical: 15 minutes

What This Alert Means

Write operations are taking longer than acceptable thresholds. This typically indicates:

  • Compaction backlog: LSM tree compaction falling behind write rate
  • WAL pressure: Write-ahead log synchronization delays
  • Consensus delays: Replication latency affecting commits

Immediate Actions

  1. [ ] Check compaction backlog size
  2. [ ] Verify disk I/O is not saturated
  3. [ ] Check WAL directory usage
  4. [ ] Review replication lag on followers

Investigation

Common Causes

  • Compaction backlog: Too many L0 files waiting for compaction
  • Disk saturation: Write bandwidth exhausted
  • WAL sync delays: Slow fsync operations
  • Consensus timeout: Network issues causing replication delays
  • Large batch sizes: Individual batches too large

Diagnostic Commands

# Check current write latency
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_write_latency_seconds

# Check compaction backlog
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytes
# > 1GB indicates significant backlog

# Check L0 file count
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_lsm_level_0_files
# > 8 files indicates compaction is behind

# Check disk I/O
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
# await > 10ms or %util > 80% indicates saturation

# Check WAL metrics
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_wal

# Check replication lag
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_replication_lag

Log Analysis

# Check for compaction stalls
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "compaction"

# Check for WAL warnings
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "wal"

# Check for consensus delays
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "commit\|consensus"

Resolution

Compaction Backlog

  1. Check backlog size:

    kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytes
  2. If > 1GB, tune compaction settings:

    # values.yaml - Phase 5 optimized defaults
    config:
      lsm_l0_compaction_trigger: 8  # Allow more L0 files before compaction
      lsm_compaction_threads: 3     # More parallel compaction
  3. For immediate relief, reduce write rate temporarily:

    • Increase batch submission interval
    • Defer non-critical writes
  4. See Compaction Backlog Runbook for detailed guidance.

Disk Saturation

  1. Check disk metrics:

    kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
  2. If saturated:

    • Upgrade to faster storage (NVMe)
    • Reduce write rate
    • Consider sharding to distribute writes

Large Batch Sizes

  1. Check batch size metrics:

    kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_batch_size
  2. If batches > 5000 events:

    • Reduce batch size to 1000-2000 for lower latency
    • Trade-off: smaller batches = lower throughput but more consistent latency

Consensus Delays

  1. Check replication lag:

    for i in 0 1 2; do
      echo -n "archerdb-$i lag: "
      kubectl exec archerdb-$i -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_replication_lag
    done
  2. If lag > 100ms, investigate network:

    kubectl exec archerdb-0 -n archerdb -- ping -c 10 archerdb-1.archerdb-headless.archerdb.svc.cluster.local
  3. See View Changes Runbook if consensus is unstable.

Tuning Write Performance

Batch Size Optimization

Batch Size Throughput Latency Use Case
100-500 Lower ~5ms P99 Latency-sensitive
500-2000 Balanced ~10ms P99 General workload
2000-5000 Higher ~25ms P99 Throughput-focused

Compaction Tuning

# For write-heavy workloads
config:
  lsm_l0_compaction_trigger: 8       # Delay compaction
  lsm_compaction_threads: 3          # More parallel work
  lsm_disable_partial_compaction: true  # Reduce compaction overhead

Prevention

  • Storage provisioning: Use NVMe with sufficient IOPS for write rate
  • Compaction headroom: Tune L0 trigger based on write patterns
  • Batch sizing: Use appropriate batch sizes for latency requirements
  • Monitoring: Alert on compaction backlog growth
  • Capacity planning: Size for peak write rates with headroom

Verification

After resolution:

# Verify write latency improved
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_write_latency_seconds

# Verify compaction backlog is decreasing
watch -n 30 'kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytes'

# Monitor for 15 minutes
watch -n 30 'kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_write_latency_seconds | grep quantile=\"0.99\"'
Edit this page

Alert: ArcherDBDiskSpaceWarning / ArcherDBDiskSpaceCritical / ArcherDBDiskFillPrediction

Quick Reference

  • Severity:
    • Warning: > 80% full OR predicted to fill in 24h
    • Critical: > 90% full OR predicted to fill in 6h
  • Metrics:
    • archerdb_storage_free_bytes
    • archerdb_storage_total_bytes
  • Threshold: Warning: 80%, Critical: 90%, Predictive: 24h/6h fill time
  • Time to Respond: Warning: 4 hours, Critical: 30 minutes

What This Alert Means

Disk space is running low or trending toward exhaustion. If the disk fills completely:

  • Writes will fail with out-of-space errors
  • Compaction will stall, causing performance degradation
  • The database may become read-only to protect data integrity

Immediate Actions

  1. [ ] Check current disk usage and free space
  2. [ ] Identify largest consumers of space
  3. [ ] Check if TTL cleanup is configured and running
  4. [ ] Assess data growth rate

Investigation

Current Disk Status

# Check disk usage via metrics
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_storage

# Check disk usage on filesystem
kubectl exec archerdb-0 -n archerdb -- df -h /data

# Check data file size
kubectl exec archerdb-0 -n archerdb -- du -sh /data/archerdb.db
kubectl exec archerdb-0 -n archerdb -- ls -la /data/

Growth Analysis

# Check entity count
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_entities_total

# Check write rate
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep 'archerdb_operations_total{.*insert'

# Check compaction status (compaction reclaims space)
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction

Common Causes

  • High ingest rate: Writing data faster than TTL can clean it
  • TTL not configured: Data accumulating without automatic cleanup
  • Compaction behind: Dead space not being reclaimed
  • Spillover files: S3 log-shipping failures causing local spillover
  • Logs/temp files: Non-database files consuming space

Resolution

Immediate Space Relief

  1. Check for non-essential files:

    kubectl exec archerdb-0 -n archerdb -- ls -la /data/
    # Look for spillover/, tmp/, or snapshot export files
  2. Check spillover directory:

    kubectl exec archerdb-0 -n archerdb -- du -sh /data/spillover/ 2>/dev/null
    # If large, check S3 log-shipping status
  3. Force compaction (recovers dead space):

    # This is automatic, but can be triggered manually
    kubectl exec archerdb-0 -n archerdb -- ./archerdb compact /data/archerdb.db

Enable/Configure TTL Cleanup

  1. Check current TTL settings:

    kubectl exec archerdb-0 -n archerdb -- ./archerdb info /data/archerdb.db | grep -i ttl
  2. Enable TTL via configuration:

    # values.yaml
    config:
      ttl_enabled: true
      ttl_default_hours: 168  # 7 days default
  3. TTL cleanup runs automatically and removes expired events during queries and compaction.

Expand Storage Capacity

For Kubernetes PVC:

  1. Check if StorageClass allows expansion:

    kubectl get storageclass -o jsonpath='{.items[*].allowVolumeExpansion}'
  2. Expand PVC:

    kubectl patch pvc data-archerdb-0 -n archerdb -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
  3. Note: Pod restart may be required for some storage classes.

For bare metal:

  1. Expand underlying storage (LVM, cloud disk, etc.)
  2. Resize filesystem: resize2fs /dev/sdX

Archive Old Data

If immediate deletion is not acceptable:

  1. Create external snapshot/archive of current data:

    # Example: archive data directory to encrypted object storage
    tar -C /data -cf - archerdb.db | aws s3 cp - s3://archive-bucket/archerdb-$(date +%Y%m%d).tar
  2. Verify archive success:

    aws s3 ls s3://archive-bucket/ | tail -n 5
  3. Consider time-based archival strategy for compliance requirements.

Prevention

Monitoring

  • Alert at 70%: Warning for early planning
  • Alert at 80%: Urgent warning
  • Alert at 90%: Critical
  • Predictive alerts: Based on growth rate

Capacity Planning

# Calculate growth rate
# Example: 10GB/day with 7-day TTL = 70GB steady state
# Add 50% headroom = 105GB minimum
Daily Ingest TTL (days) Steady State Recommended Size
1 GB 7 7 GB 15 GB
10 GB 7 70 GB 110 GB
10 GB 30 300 GB 450 GB
100 GB 7 700 GB 1 TB

Retention Policies

  1. Set appropriate TTL for your use case
  2. Use time-partitioned groups for easier archival
  3. Implement data lifecycle policies

Emergency Procedures

If Disk is 100% Full

  1. Database may be read-only. Immediate action required.

  2. Free emergency space:

    # Remove any non-essential files
    kubectl exec archerdb-0 -n archerdb -- rm -rf /data/tmp/* 2>/dev/null
    kubectl exec archerdb-0 -n archerdb -- rm -rf /data/spillover/* 2>/dev/null
  3. If database is read-only, restart after freeing space:

    kubectl delete pod archerdb-0 -n archerdb
  4. Expand storage immediately (see Expand Storage Capacity above).

Emergency Data Deletion

Warning: This deletes data permanently.

# Delete all expired events immediately
./archerdb ttl-cleanup --force /data/archerdb.db

# Delete events older than specific time
./archerdb cleanup --older-than=2024-01-01 /data/archerdb.db

Verification

After resolution:

# Verify disk usage decreased
kubectl exec archerdb-0 -n archerdb -- df -h /data

# Verify metrics updated
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_storage

# Monitor growth rate for next hour
watch -n 60 'kubectl exec archerdb-0 -n archerdb -- df -h /data'
Edit this page

Alert: ArcherDBCompactionBacklog

Quick Reference

  • Severity: warning
  • Metric: archerdb_compaction_pending_bytes
  • Threshold: > 1GB pending compaction
  • Time to Respond: Within 1 hour

What This Alert Means

LSM tree compaction is falling behind the write rate. When compaction cannot keep up:

  • Read latency increases (more levels to search)
  • Write latency may spike (write stalls when L0 full)
  • Disk usage grows faster than expected

Immediate Actions

  1. [ ] Check current L0 file count
  2. [ ] Verify compaction is running
  3. [ ] Check disk I/O capacity
  4. [ ] Assess write rate vs compaction throughput

Investigation

Common Causes

  • High write rate: Writes exceeding compaction capacity
  • Under-provisioned disk: I/O bandwidth insufficient
  • CPU-limited compaction: Not enough compaction threads
  • Large L0 trigger: Compaction starting too late

Diagnostic Commands

# Check compaction backlog size
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytes

# Check L0 file count (trigger point)
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_lsm_level_0_files
# > 8 files means compaction trigger reached

# Check if compaction is active
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_active
# 1 = running, 0 = idle

# Check compaction throughput
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_bytes_written

# Check write rate
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep 'archerdb_bytes_written_total'

# Check disk I/O
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5

Log Analysis

# Check for compaction events
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "compaction"

# Check for write stalls
kubectl logs archerdb-0 -n archerdb --since=1h | grep -i "stall\|pause"

Resolution

Tune Compaction Settings

Phase 5 optimization established these defaults for write-heavy workloads:

# values.yaml
config:
  lsm_l0_compaction_trigger: 8       # Allow 8 L0 files before compaction
  lsm_compaction_threads: 3          # 3 parallel compaction threads
  lsm_disable_partial_compaction: true  # Complete compactions only
  1. Increase compaction threads (if CPU available):

    lsm_compaction_threads: 4  # Increase from 3
  2. Adjust L0 trigger (trade-off: higher = more read amplification):

    lsm_l0_compaction_trigger: 12  # Allow more L0 buildup
  3. Apply changes with rolling restart:

    kubectl rollout restart statefulset/archerdb -n archerdb

Reduce Write Rate

If compaction cannot keep up even with tuning:

  1. Temporary relief: Increase batch submission interval
  2. Defer non-critical writes during peak hours
  3. Consider rate limiting at application layer

Upgrade Storage

If disk I/O is the bottleneck:

  1. Check current I/O utilization:

    kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5
    # %util > 80% indicates saturation
  2. Upgrade to faster storage:

    • NVMe strongly recommended for production
    • Target: > 100K IOPS for heavy write workloads

Horizontal Scaling (Sharding)

For sustained high write rates beyond single-node capacity:

  1. Consider adding shards to distribute write load
  2. Each shard handles a subset of data
  3. See Sharding Strategy

Understanding LSM Compaction

How Compaction Works

Writes -> Memtable -> L0 (immutable) -> L1 -> L2 -> ...

When L0 file count reaches trigger (8 by default):
  - Compaction merges L0 files into L1
  - This continues down levels as needed
  - Older/deleted data is discarded

Compaction Tuning Trade-offs

Setting Higher Value Lower Value
l0_compaction_trigger More write throughput, higher read amp More compaction, lower read amp
compaction_threads Faster compaction, more CPU Slower compaction, less CPU

Write Amplification

Compaction causes write amplification (data written multiple times during compaction):

  • Expected: 10-30x write amplification
  • If higher, consider tuning or sharding
# Check write amplification
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_write_amp

Prevention

  • Capacity planning: Size disk I/O for peak write rate + compaction overhead
  • Monitoring: Alert when backlog > 500MB (early warning)
  • Tuning: Establish baseline and tune for your workload
  • Storage tier: Use NVMe for write-heavy workloads

Verification

After resolution:

# Verify backlog is decreasing
watch -n 30 'kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytes'

# Verify L0 file count is healthy
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_lsm_level_0_files
# Should be < trigger value

# Verify latency improved
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_write_latency_seconds
Edit this page

Runbook — Kernel-crash durability verification

This runbook drives the scripts/durability-kernel-crash.sh harness, which boots ArcherDB inside a QEMU VM, runs a write workload, hard-resets the VM from the QEMU monitor, and then verifies that the data file is still consistent on the next boot.

It catches regressions in the checkpoint + WAL replay path that local-only crash testing (SIGKILL + restart) cannot catch: dirty pages held in the guest kernel page cache, disk cache flushing, fsync ordering under I/O pressure, and controller write reordering.

When to run

  • After any change to: src/vsr/journal.zig, src/storage.zig, src/lsm/grid.zig, the checkpoint artifact path (src/archerdb/checkpoint_artifact.zig), or the underlying IO plumbing in src/io/linux.zig.
  • Before a release candidate that touches durability claims in the operations runbook.
  • When investigating a field report of “replica lost data after a power event”.

When NOT to run

  • For routine changes unrelated to durability (CI’s VOPR is sufficient).
  • On machines without KVM access — the harness falls back to TCG, which is 5–10× slower and usually not worth the wall-clock time.

Prerequisites

On the host:

sudo apt install qemu-system-x86 qemu-utils cloud-image-utils genisoimage socat
sudo usermod -aG kvm "$USER"   # then re-login so /dev/kvm is accessible

Download a base cloud image once:

cd ~/ci-assets
wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

Run

./scripts/durability-kernel-crash.sh \
    --image ~/ci-assets/noble-server-cloudimg-amd64.img \
    --crash-after 60s

The script builds ArcherDB from source, boots a VM, launches archerdb start plus a continuous insert workload driven by archerdb repl, waits for --crash-after seconds, then issues system_reset via the QEMU monitor. It reboots the same data disk and runs archerdb verify. Exit code:

  • 0PASS: kernel-crash durability — data file verified after hard reset
  • 1FAIL: archerdb verify did not succeed (recovery log tail is printed)
  • 2 — setup error (missing tool, missing image)

Expected runtime

  • Build: ~2 min on a fresh machine, cached afterward.
  • Workload + crash: --crash-after seconds (default 60).
  • Recovery boot + verify: ~30–60 s under KVM, up to 5 min under TCG.

Tuning knobs

Flag Default When to change
--crash-after 60s Longer to exercise more checkpoints; shorter for a quick smoke.
--ram-mb 2048 Increase if you see OOM kills in recovery.log.
--data-disk-gb 2 Increase if the workload generates more than ~1 GB of LSM data.
--workload-rate 200 events/sec Cranking this up stresses the write path more aggressively.
--keep-artifacts off Keeps the VM overlays, data disk, and logs under $WORKDIR for forensics.

Interpreting failures

FAIL: recovery did not complete within 120s The recovery VM hung before archerdb verify printed a result. Common causes:

  • The replica got stuck replaying a large WAL — grow --ram-mb and retry.
  • A deadlock in the recovery path — capture cat "$WORKDIR/recovery.log" and open an issue.

FAIL: archerdb verify did not succeed; recovery log follows: The replica booted but reported corrupted or missing data. This is the actual regression signal. Grab the recovery log plus the $WORKDIR/data.qcow2 and reproduce under archerdb inspect for a closer look. Preserve the workdir with --keep-artifacts.

Workload writer inside the VM is idle Check /var/log/workload.log via archerdb repl-from-host. If it is empty, cloud-init probably did not pick up the binary ISO — verify the base image supports cloud-init v24+.

Known limitations

  • Not a CI gate yet. Running this harness needs a prebuilt cloud image and ~2 minutes of QEMU wall-clock per run; the CI workers don’t have either today. When they do, this runbook will be supplemented by a nightly CI lane.
  • Does not test disk-firmware-level misbehavior. Real controllers can reorder writes across power cuts in ways cache=none,aio=native does not model. For that, combine this harness with dm-flakey-based injection on the host — scripts/dm_flakey_test.sh exists as a starting point.
  • Single replica. Multi-replica kernel-crash scenarios (one replica crashes while its peers keep serving) need cluster-scale orchestration — that is a separate harness.

See also

  • src/testing/storage.zig — simulated Storage with optional available_capacity for modeling disk pressure without a VM.
  • scripts/dm_flakey_test.sh — block-layer fault injection on the host.
  • scripts/chaos-test.sh — process-level crash/restart exercise (no VM).
  • Production ENOSPC note at src/io/linux.zig:1660.
Edit this page

Benchmark Framework

This document describes ArcherDB’s benchmark evidence model and where benchmark artifacts live.

Current Evidence

The latest measured single-node comparison (ArcherDB vs Valkey vs PostGIS on identical hardware, with methodology and reproduction steps) is benchmarks/single-node-2026-08.md. Raw run outputs: reports/benchmarks/evidence-20260803/.

Two methodology rules learned from that exercise are now binding:

  1. Insert throughput requires sustained load. One full-size message carries ~81,916 events, so runs below a few hundred thousand events measure a handful of batches (startup-dominated), not throughput. The CI quick lane now inserts 500K events for exactly this reason.
  2. Cross-store comparisons must state durability. ArcherDB replies after fsync + consensus; Valkey’s default replies from memory. Always publish the volatile ceiling and the durability-matched number (appendfsync always) alongside ArcherDB results.

Benchmark Layers

ArcherDB currently has three benchmark layers in the repository:

  1. The Python benchmark harness under test_infrastructure/benchmarks/
  2. GitHub Actions workflows for baseline comparison and published history under .github/workflows/benchmark.yml and .github/workflows/benchmark-weekly.yml
  3. The correctness-gated external comparison suite under scripts/competitor-benchmarks/v2/

Local Outputs

Local benchmark runs write to:

  • reports/benchmarks/ for detailed run outputs
  • reports/history/ for local history
  • reports/baselines/ for saved baselines

These are the authoritative local paths for development and release-candidate evidence.

Published History

The manual benchmark publication workflow stores promoted historical artifacts in:

  • benchmarks/history/

Use that location for checked-in benchmark snapshots and long-term graph publication. Do not assume it is populated unless the publication workflow has been run.

The validated August 2026 ArcherDB Lite, PostgreSQL + PostGIS, and Valkey comparisons (two profiles: bulk 80,000-event batches and SDK-default 1,000-event batches, both regenerated after the client-SDK fixes; the August 2 run remains in history as the pre-fix baseline) are published at:

  • benchmarks/history/2026-08-04-comparison-v2.json
  • benchmarks/history/2026-08-03-comparison-v2.json
  • benchmarks/comparison-v2/report.md
  • benchmarks/comparison-v2/analysis.ipynb

Its public-claim gate passes only for equal-semantics full-history ingestion: every system durably retained and geospatially indexed all events while maintaining a separate latest-position index. Read/query results remain context-only where the five-trial coefficient of variation exceeds 10%.

CLI Surface

The benchmark CLI is the supported entry point for local execution:

# Single topology
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60 --op-count 10000

# Full suite
python3 test_infrastructure/benchmarks/cli.py run --full-suite

# Full suite without mixed workload
python3 test_infrastructure/benchmarks/cli.py run --full-suite --no-mixed

The active harness now drives ArcherDB through the supported SDK/client surface. It does not send raw HTTP requests at replica/message-bus ports.

For larger local topologies, the harness also uses a machine-fit cluster profile so 5-node and 6-node runs do not reserve the same per-node memory budget as 1-node and 3-node runs on a shared development machine.

For the 6-node local topology, the harness now also:

  • formats the cluster as 5 voters plus 1 standby
  • passes --replica-count through both format and start
  • staggers startup until each replica completes local init
  • orders SDK endpoints as leader-first, then the remaining voters

Current checked-in quick artifacts from the April 9, 2026 evidence refresh live under:

  • reports/benchmarks/release-20260409-sdkquick/20260409-055134-1node.json
  • reports/benchmarks/release-20260409-sdkquick/20260409-055217-3node.json
  • reports/benchmarks/release-20260409-sdkquick/20260409-063550-5node.json
  • reports/benchmarks/release-20260409-sdkquick/20260409-082107-6node.json

Performance Targets

The repository currently uses these comparison gates on comparable hardware profiles:

Metric Baseline Target Stretch Target
3-node throughput >=770K events/sec >=1M events/sec
Read latency P95 <1ms <0.5ms
Read latency P99 <10ms <5ms
Write latency P95 <10ms <5ms
Write latency P99 <50ms <25ms

Release Rule

Performance claims in release docs and announcements should only cite benchmark artifacts produced by the real benchmark harness. Synthetic proxies and stale historical summaries are not sufficient release evidence.

Edit this page

ArcherDB Benchmark Guide

Guide for running, interpreting, and tracking ArcherDB performance benchmarks.

Overview

ArcherDB benchmarks measure:

  • Throughput: Events processed per second
  • Read latency: Query response time percentiles
  • Write latency: Insert response time percentiles
  • Mixed workload: Combined read/write performance
  • Scaling: Performance across topologies (1/3/5/6 nodes)

Performance Targets

The repository uses the following comparison gates on comparable hardware profiles:

Metric Baseline Target Stretch Target
3-node throughput >=770K events/sec >=1M events/sec
Read latency P95 <1ms <0.5ms
Read latency P99 <10ms <5ms
Write latency P95 <10ms <5ms
Write latency P99 <50ms <25ms

Running Benchmarks Locally

Prerequisites

# Install benchmark dependencies
pip install -r test_infrastructure/requirements.txt

# Build ArcherDB (lite config for testing)
./zig/zig build -j4 -Dconfig=lite

Quick Run (Single Topology)

# Run all benchmark types on 3-node cluster
python3 test_infrastructure/benchmarks/cli.py run --topology 3

# With time limit
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60

# With operation count limit
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --op-count 10000

Full Suite (All Topologies)

# Run complete benchmark suite (1/3/5/6 node topologies)
python3 test_infrastructure/benchmarks/cli.py run --full-suite

# Exclude mixed workload tests (faster)
python3 test_infrastructure/benchmarks/cli.py run --full-suite --no-mixed

Mixed Workload Benchmarks

Control the read/write ratio:

# 80% reads, 20% writes (default)
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.8

# 50% reads, 50% writes
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.5

# Write-heavy (20% reads, 80% writes)
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 0.2

# Read-only
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --read-write-ratio 1.0

Compare to Baseline

# Compare current run to stored baseline
python3 test_infrastructure/benchmarks/cli.py compare baseline.json current.json

Interpreting Results

Throughput

Events processed per second. Higher is better.

Throughput: <measured events/sec>
Compare against: the latest checked-in baseline for the same hardware/profile
Status: PASS when the run meets or exceeds the local comparison gate

Key factors:

  • Batch size (larger batches = higher throughput)
  • Network latency (lower = higher throughput)
  • Node count (more nodes = higher total throughput, but overhead)

Latency Percentiles

Query response times. Lower is better.

Read Latency:
  P50: 0.3ms  (median)
  P95: 0.8ms  (95% of requests)
  P99: 4.2ms  (99% of requests)

Target: P95 <1ms, P99 <10ms
Status: PASS

Percentile meanings:

  • P50 (median): Typical user experience
  • P95: 95% of requests are this fast or faster
  • P99: Captures tail latency, important for SLAs

Confidence Intervals

All means are reported with 95% confidence intervals:

P95: 0.8ms +/- 0.1ms (95% CI)

Narrower intervals = more stable measurements. Wide intervals suggest:

  • Insufficient samples
  • High variance in measurements
  • System noise

Coefficient of Variation (CV)

Measures result stability:

CV: 8.2% (target: <10%)
  • <10%: Results are stable, trustworthy
  • 10-20%: Somewhat noisy, consider more samples
  • >20%: High variance, investigate system state

Regression Detection

Threshold

A regression is detected when:

  • Performance degrades by >10% from baseline
  • Statistical test confirms significance (p < 0.05)

Statistical Method

We use Welch’s t-test (unequal variance):

from scipy.stats import ttest_ind
t_stat, p_value = ttest_ind(baseline, current, equal_var=False)

Benefits:

  • Does not assume equal variance between runs
  • Robust to different sample sizes
  • Standard statistical rigor

Comparison Report

Regression Analysis
==================
Baseline: previous checked-in artifact
Current:  current run artifact

Change: <measured delta>
p-value: <measured significance>
Status: REGRESSION DETECTED when the comparison crosses the configured threshold

Recommendation: Investigate recent changes when the current run underperforms the baseline

Historical Tracking

Manual Publication Runs

Maintainers can run the publication workflow manually and promote benchmark results into checked-in history:

benchmarks/history/
  2026-01-05.json
  2026-01-12.json
  2026-01-19.json
  2026-01-26.json
  2026-02-02.json
  ...

Baseline Files

Local baselines for regression detection live under:

reports/baselines/
  baseline-1node-*.json
  baseline-3node-*.json
  baseline-5node-*.json
  baseline-6node-*.json

Visualization

Results are visualized using github-action-benchmark:

  • Throughput graph: Events/sec over time
  • Latency graph: P95/P99 over time
  • Scaling graph: Performance vs node count

View at: https://github.com/[org]/archerdb/benchmarks

CI Integration

Publication Workflow

The benchmark-weekly.yml workflow:

  1. Spins up clusters (1/3/5/6 nodes)
  2. Runs full benchmark suite
  3. Compares to baseline
  4. Alerts on >10% regression
  5. Can promote approved results into benchmarks/history/
  6. Updates benchmark graphs

Alerts

On regression detection:

  • Workflow fails (visible in GitHub)
  • Comment posted on triggering commit
  • GitHub issue created with details
  • Slack notification (if configured)

Manual Trigger

# Trigger weekly benchmark manually
gh workflow run benchmark-weekly.yml

Programmatic Usage

from test_infrastructure.benchmarks import BenchmarkOrchestrator, BenchmarkConfig

# Create orchestrator
orchestrator = BenchmarkOrchestrator()

# Configure benchmark
config = BenchmarkConfig(
    topology=3,
    time_limit_sec=60,
    op_count_limit=10_000,
    read_write_ratio=0.8,  # 80% reads, 20% writes
)

# Run individual benchmarks
throughput = orchestrator.run_throughput_benchmark(3, config)
read_latency = orchestrator.run_latency_read_benchmark(3, config)
write_latency = orchestrator.run_latency_write_benchmark(3, config)
mixed = orchestrator.run_mixed_workload_benchmark(3, config)

# Access results
print(f"Throughput: {throughput['throughput_events_per_sec']}")
print(f"Read P95: {read_latency['p95_ms']}ms")
print(f"Write P95: {write_latency['p95_ms']}ms")

# Run full suite
results = orchestrator.run_full_suite(
    topologies=[1, 3, 5, 6],
    include_mixed=True,
)

Output Formats

Format Location Purpose
JSON reports/benchmarks/*.json CI automation, data processing
CSV reports/benchmarks/*.csv Spreadsheet analysis
Terminal stdout Interactive feedback
Markdown Updated docs Human review

JSON Format

{
  "timestamp": "2026-02-01T02:00:00Z",
  "topology": 3,
  "throughput": {
    "events_per_sec": 823456,
    "target": 770000,
    "passed": true
  },
  "read_latency": {
    "p50_ms": 0.3,
    "p95_ms": 0.8,
    "p99_ms": 4.2,
    "samples": 10000
  },
  "write_latency": {
    "p50_ms": 2.1,
    "p95_ms": 8.5,
    "p99_ms": 42.3,
    "samples": 2000
  },
  "metadata": {
    "version": "1.0.0",
    "git_sha": "abc1234",
    "runner": "ubuntu-latest-8-cores"
  }
}

Best Practices

Consistent Environment

  • Use dedicated hardware or CI runners
  • Close other applications during local runs
  • Use the same build configuration tier (for example, lite vs standard)

Warm-up

SDKs with JIT compilation (Java, Node.js) need warm-up:

SDK Recommended Warm-up
Java 500 iterations
Node.js 200 iterations
Python 100 iterations
Go 100 iterations
C 50 iterations

Sample Size

  • Minimum 1000 samples for percentile accuracy
  • Continue until CV < 10% (stability check)
  • Maximum 10 stability check rounds

Fresh Cluster

Each benchmark run should use a fresh cluster to ensure isolated measurements without accumulated state affecting results.

Troubleshooting

Results Vary Widely

  • Increase sample count
  • Check for background processes
  • Verify network stability
  • Use constrained build (-Dconfig=lite)

Benchmarks Hang

  • Check server health: curl http://localhost:3001/ping
  • Verify cluster formed: curl http://localhost:3001/topology
  • Check logs for errors

Results Don’t Match CI

  • Use same hardware profile as CI
  • Use same build configuration
  • Account for warm-up differences

See Also


Last updated: 2026-02-01

Edit this page

Single-Node Benchmark Evidence — August 2026

This document records a measured, reproducible single-node comparison of ArcherDB against Valkey and PostGIS on identical hardware, using the repo’s own harnesses. Raw outputs live in reports/benchmarks/evidence-20260803/.

Headline (single node, all ArcherDB writes durable + checksummed):

System Configuration Insert throughput
ArcherDB (native client, max batches) durable, consensus-committed 831,000 events/s
ArcherDB (Python SDK, pipelined) durable, consensus-committed 659,000 events/s
Valkey 8.1 GEOADD (best of any pipelining) no persistence 174,000–202,000 ops/s
Valkey 8.1 GEOADD (appendfsync always, pipelined) durable per op 109,000 ops/s
Valkey 8.1 GEOADD (appendfsync always, sequential) durable per op 1,474 ops/s
PostGIS 16 (GIST, batched inserts) durable, no consensus 29,000–31,000 rows/s

ArcherDB’s durable, replicable write path is 4–4.8× faster than Valkey’s volatile GEOADD ceiling, 7.6× faster than Valkey configured for comparable durability, and ~27× faster than PostGIS — while also serving spatial queries with sub-millisecond p99 latency.

Test environment

  • AMD EPYC (KVM guest), 8 vCPUs, 24 GB RAM, Linux 6.8.0-124-generic.
  • Virtio disk: ~571 µs per 4 KiB O_DIRECT+O_DSYNC write (7.2 MB/s), 901 MB/s for 1 MiB durable writes. Datacenter NVMe with power-loss protection has 10–20× lower sync-write latency, which disproportionately improves ArcherDB’s small-batch and Valkey’s fsync-always numbers.
  • ArcherDB: -Drelease -Dconfig=lite, single replica, O_DIRECT + O_DSYNC WAL (two durable writes per request), Aegis-128L checksums on every message. The lite tier shares the 10 MiB message envelope with all production tiers.
  • Valkey 8.1.9: official docker image, loopback inside the container netns. Default configuration (--save '' --appendonly no, io-threads=1) unless noted; the fsync rows use appendonly yes + appendfsync always.
  • PostGIS 16-3.4: official docker image, schema + GIST index from scripts/competitor-benchmarks/setup-postgis.sh, psycopg2.execute_values batched inserts.
  • Runs are serial; each measurement waits for system load < 2.0.

ArcherDB: insert throughput vs. request batch size

Native load generator (archerdb benchmark, in-process VSR client, one request in flight per client). Every request is consensus-committed and durable on disk before its reply.

Events per request Clients Events Throughput
240 1 200K 182,586 events/s
1,000 1 500K 344,232 events/s
8,000 1 1M 620,143 events/s
8,000 8 1M 637,246 events/s
65,535 1 1M 661,747 events/s
81,916 (max) 1 1M 901,823 events/s
81,916 (max) 1 2M 830,677 events/s

Reading the curve:

  • Small requests are bounded by the durable-write floor: each request pays two serialized O_DIRECT+O_DSYNC writes (~1.2 ms on this disk) regardless of size. Batching amortizes that cost; this is the design point, not a trick — one 10 MiB request carries up to 81,916 128-byte events.
  • At large batches the single replica core becomes execute-bound (~1 µs/event: S2 cell computation, cuckoo RAM-index upsert, LSM insert).
  • Concurrent clients neither help nor hurt materially at equal event totals (8,000-event batches: 620K/s at 1 client vs 637K/s at 8) — single-node writes are intentionally sequential in the WAL for crash-recovery correctness. Sustained multi-million-event runs shed ~8% (max batches) to ~25% (8K batches) to LSM compaction pacing; reducing per-commit compaction overhead at high commit rates is tracked as follow-up work.

ArcherDB: query latency (single node, 2M events loaded)

Measured in the same run as the 2M-event insert (N5-bmax-c1.txt):

Query p50 p99
UUID point lookup 92 µs 323 µs
UUID batch (per entity) 1 µs 3 µs
Radius (1 km) 91 µs 191 µs
Polygon 87 µs 327 µs

PostGIS on the same box and dataset shape: UUID lookup 3,461/s (~289 µs each, best case, no concurrency), radius 1,508/s (~663 µs), polygon 5.6/s (~178 ms).

Python SDK

All runs: fresh single-node server, 393K–400K events, zero errors.

Shape Throughput
benchmark_geo.py defaults (8,000-event requests, sequential) 421,282 events/s
65,535-event requests, sequential 475,540 events/s
insert_events_pipelined (8,000-event requests, window 8) 659,389 events/s
same shape, strictly synchronous 316,514 events/s

Pipelining (GeoClientSync.insert_events_pipelined / NativeClient.insert_events_pipelined) overlaps client-side packing with server round trips for a 2.1× gain over synchronous submission at the same batch size.

Before the August 2026 fixes the same harness measured 59–71K events/s: it capped requests at 240 events (a stale 32 KiB assumption) and the SDK built events one ctypes field at a time. If you have older numbers on file, they measured the harness, not the server.

Valkey comparison notes

  • GEOADD is Valkey’s closest analog to an ArcherDB event insert. Its ceiling on this box is ~174–202K ops/s regardless of pipelining depth or connection count (single-threaded server, ~5 µs/op); plain SET reaches 752K ops/s, which bounds any Valkey-based design from above.
  • Default Valkey acknowledges writes from memory only. A crash loses every write since the last RDB snapshot — with snapshots disabled (the default --save '' in many deployments), the entire dataset. ArcherDB replies only after the write is fsync’d and consensus-committed.
  • With appendfsync always (the durability-comparable configuration), Valkey drops to 109K ops/s pipelined — and 1,474 ops/s for sequential clients, because each op pays the same ~0.6 ms sync-write cost that ArcherDB amortizes across up to 81,916 events per request.

Reproduction

# ArcherDB native curve (fresh temp server per run):
./zig/zig build -Drelease -Dconfig=lite
./zig-out/bin/archerdb benchmark --event-count=1000000 --event-batch-size=8000
./zig-out/bin/archerdb benchmark --event-count=2000000   # max batches

# Python SDK (server on :3001):
./zig-out/bin/archerdb format --cluster=0 --replica=0 --replica-count=1 0_0.archerdb
./zig-out/bin/archerdb start --addresses=127.0.0.1:3001 0_0.archerdb &
python3 benchmark_geo.py --events 400000 --batch-size 8000 --addresses 127.0.0.1:3001

# Valkey (docker):
docker run -d --name valkey valkey/valkey:8.1 valkey-server --save '' --appendonly no
docker exec valkey valkey-benchmark -n 2000000 -c 8 -P 64 -r 10000000 -q \
  GEOADD fleet 13.361389 38.115556 m:__rand_int__

# PostGIS (docker):
docker run -d --name postgis -e POSTGRES_USER=bench -e POSTGRES_PASSWORD=bench \
  -e POSTGRES_DB=geobench -p 127.0.0.1:5433:5432 postgis/postgis:16-3.4
# schema: scripts/competitor-benchmarks/setup-postgis.sh
python3 scripts/competitor-benchmarks/benchmark-postgis.py --port 5433 \
  --event-count 200000 --batch-size 8000 --json

Caveats: single-node lite tier on shared virtualized hardware; a background load gate (< 2.0) was enforced but the host is not dedicated. Numbers on dedicated hardware with power-loss-protected NVMe will be higher, especially for small batches. Multi-node cluster benchmarks are tracked separately (docs/BENCHMARKS.md targets: ≥770K events/s on 3 nodes).

Edit this page

ArcherDB Profiling Guide

This guide covers CPU profiling and performance analysis for ArcherDB using Linux perf and flame graphs.

Table of Contents

Prerequisites

System Requirements

  • Linux kernel >= 5.6 with perf support
  • perf tools installed
  • FlameGraph scripts for flame graph generation

Install perf

# Ubuntu/Debian
sudo apt install linux-perf

# If that doesn't work, try version-specific package
sudo apt install linux-tools-$(uname -r)

# Fedora/RHEL
sudo dnf install perf

# Arch
sudo pacman -S perf

Install FlameGraph

# Clone into the project tools directory
git clone https://github.com/brendangregg/FlameGraph.git tools/FlameGraph

# Or set FLAMEGRAPH_DIR to use an existing installation
export FLAMEGRAPH_DIR=/path/to/FlameGraph

Configure perf Permissions

By default, perf requires elevated privileges. To enable user profiling:

# Check current setting
cat /proc/sys/kernel/perf_event_paranoid

# Allow user profiling (value of 1 or lower)
sudo sysctl kernel.perf_event_paranoid=1

# Make permanent (add to /etc/sysctl.conf)
echo "kernel.perf_event_paranoid=1" | sudo tee -a /etc/sysctl.conf

Frame Pointers

ArcherDB builds preserve frame pointers (-fno-omit-frame-pointer), ensuring complete stack traces in profiling output. You should not see [unknown] frames in flame graphs.

Quick Start

Generate a Flame Graph

# Build with release optimizations
./zig/zig build -Drelease

# Profile a benchmark run
./scripts/flamegraph.sh --output profile.svg -- ./zig-out/bin/archerdb benchmark

# View in browser
xdg-open profile.svg
# Or: firefox profile.svg
# Or: google-chrome profile.svg

Collect Hardware Counters

# Profile hardware counters (5 runs for statistics)
./scripts/profile.sh -- ./zig-out/bin/archerdb benchmark

Flame Graphs

Flame graphs provide hierarchical visualization of where CPU time is spent.

Using flamegraph.sh

# Basic usage - profile a command
./scripts/flamegraph.sh --output profile.svg -- ./zig-out/bin/archerdb benchmark

# Profile an existing server process
./scripts/flamegraph.sh --output server.svg --pid 12345 --duration 60

# High-frequency sampling with kernel stacks
./scripts/flamegraph.sh --output detailed.svg -f 999 -a -- ./zig-out/bin/archerdb benchmark

Options

-d, --duration <sec>    Sampling duration in seconds (default: 30)
-f, --frequency <hz>    Sampling frequency in Hz (default: 99)
-a, --all               Include kernel stacks
-p, --pid <pid>         Attach to existing process
--output <file.svg>     Output SVG file (required)
--no-cleanup            Keep perf.data file after generation

How to Read Flame Graphs

Flame graphs are a visualization of CPU samples collected during program execution.

  • X-axis (width): Time spent in a function. Wider = more CPU time.
  • Y-axis (height): Stack depth. Bottom = entry point, top = leaf functions.
  • Colors: Arbitrary (no meaning), just for visual distinction.
  • Interactivity: Click on a frame to zoom in, click “Reset Zoom” to return.

Common Patterns

Hot Functions (Wide Bars)

Wide bars at the top of the stack indicate functions consuming significant CPU time:

|                      hot_function                       |  <- Optimize this
|            caller_1             |       caller_2        |
|                         main                            |

Action: Focus optimization efforts on these functions.

Deep Stacks

Very tall stacks may indicate excessive abstraction or recursion:

|func|
|func|
|func|
...
|main|

Action: Consider flattening call chains or converting recursion to iteration.

Unexpected Callers

If a function appears under unexpected parents, trace the call path:

|      slow_path     |
|   unexpected_fn    |  <- Why is this calling slow_path?
|       main         |

Action: Investigate whether this code path should exist.

ArcherDB-Specific Tips

When profiling ArcherDB, look for:

  1. S2 Cell Operations: Functions in s2/ directory handling spatial indexing
  2. LSM Operations: Compaction, level management in the storage layer
  3. Network I/O: Message serialization/deserialization
  4. Memory Allocation: Frequent allocator calls may indicate optimization opportunities

Hot spots to watch:

  • s2.region_coverer.getCovering - Spatial query coverage
  • ram_index operations - In-memory indexing
  • io_uring submission paths - Async I/O handling

Hardware Counter Profiling

Using profile.sh

The scripts/profile.sh script collects hardware performance counters:

# Basic profiling (5 runs)
./scripts/profile.sh -- ./zig-out/bin/archerdb benchmark

# More runs for better statistics
./scripts/profile.sh --repeat 10 -- ./zig-out/bin/archerdb benchmark

# JSON output for CI
./scripts/profile.sh --json -- ./zig-out/bin/archerdb benchmark > metrics.json

# Per-core breakdown
./scripts/profile.sh --detailed -- ./zig-out/bin/archerdb benchmark

# Custom counters
./scripts/profile.sh -c cycles,instructions,L1-dcache-load-misses -- ./zig-out/bin/archerdb benchmark

Options

-c, --counters <list>   Hardware counters, comma-separated
                        (default: cycles,instructions,cache-misses,cache-references,branch-misses,branches)
-r, --repeat <n>        Number of runs for statistics (default: 5)
-d, --detailed          Show detailed per-core breakdown
--json                  Output in JSON format for CI

Key Metrics

Instructions Per Cycle (IPC)

IPC measures how efficiently the CPU executes instructions:

IPC Range Interpretation
< 1.0 CPU-bound with many stalls (cache misses, branch mispredictions)
1.0-2.0 Typical for complex workloads
2.0-4.0 Very efficient, well-optimized code
> 4.0 Possible with SIMD/vectorization

For ArcherDB workloads: Expect IPC of 1.0-2.0 for typical database operations. Lower values during spatial queries (complex branching) are normal.

Cache Miss Rate

Percentage of memory accesses that miss the last-level cache:

Rate Interpretation
< 5% Excellent cache behavior
5-10% Good
10-20% Acceptable for large datasets
> 20% Potential memory bottleneck

For ArcherDB: Higher miss rates during full-table scans are expected. Point lookups should have < 5% miss rate.

Branch Miss Rate

Percentage of branch instructions that were mispredicted:

Rate Interpretation
< 2% Excellent branch prediction
2-5% Typical
> 5% May benefit from branchless code

For ArcherDB: Spatial queries have inherently unpredictable branches. Focus optimization on frequently executed paths.

Hardware Counters Reference

Counter Meaning Good Value
cycles CPU cycles consumed Lower is better
instructions Instructions executed Context-dependent
cache-references L1/L2/L3 cache accesses N/A (informational)
cache-misses Cache misses Lower is better
branches Branch instructions N/A (informational)
branch-misses Mispredicted branches Lower is better
L1-dcache-loads L1 data cache loads N/A (informational)
L1-dcache-load-misses L1 data cache load misses Lower is better

Profiling Workflows

Profiling a Specific Benchmark

# Build release binary
./zig/zig build -Drelease

# Profile the benchmark command
./scripts/flamegraph.sh --output insert.svg --duration 60 -- \
    ./zig-out/bin/archerdb benchmark --event-count 1000000

# Collect hardware counters
./scripts/profile.sh --repeat 10 -- \
    ./zig-out/bin/archerdb benchmark --event-count 100000

Profiling a Running Server

# Start ArcherDB in one terminal
./zig-out/bin/archerdb --data-dir /tmp/archerdb

# Find the PID
pgrep archerdb

# Profile for 60 seconds
./scripts/flamegraph.sh --output server.svg --pid $(pgrep archerdb) --duration 60

# Generate load in another terminal while profiling
./zig-out/bin/archerdb benchmark --addresses 127.0.0.1:3001

A/B Comparison Workflow

For comparing performance before/after changes:

# 1. Profile baseline (before changes)
git stash  # Save your changes
./zig/zig build -Drelease
./scripts/profile.sh --json -- ./zig-out/bin/archerdb benchmark > baseline.json
./scripts/flamegraph.sh --output baseline.svg -- ./zig-out/bin/archerdb benchmark

# 2. Profile with changes
git stash pop
./zig/zig build -Drelease
./scripts/profile.sh --json -- ./zig-out/bin/archerdb benchmark > changed.json
./scripts/flamegraph.sh --output changed.svg -- ./zig-out/bin/archerdb benchmark

# 3. Compare JSON metrics
diff baseline.json changed.json

# 4. Visual comparison - open both flame graphs side by side

Continuous Profiling in CI

# In CI pipeline
./scripts/profile.sh --json --repeat 10 -- ./zig-out/bin/archerdb benchmark > metrics.json

# Check for regressions
# (compare against baseline metrics stored in repo or artifact storage)

A/B Benchmarking with POOP

POOP (Performance Optimizer Observation Platform) enables statistical comparison between two versions with hardware counter analysis.

Why POOP over Hyperfine?

Feature POOP Hyperfine
Hardware counters Yes (cycles, cache misses, branches) No
Statistical comparison First command as baseline Manual calculation
IPC calculation Built-in No
Zig integration Native External

Installation

# Clone and build POOP
git clone https://github.com/andrewrk/poop tools/poop
cd tools/poop && zig build -Doptimize=ReleaseFast

# Binary at tools/poop/zig-out/bin/poop
# Or set POOP_PATH environment variable
export POOP_PATH=/path/to/poop

Basic Usage

# Compare two binaries directly with POOP
./tools/poop/zig-out/bin/poop \
  './archerdb-v1 benchmark' \
  './archerdb-v2 benchmark'

Note: For wrapper script usage and detailed POOP workflow, see plan 11-02.

Memory Profiling

Using DebugAllocator

ArcherDB includes a TrackingAllocator wrapper for memory analysis:

const tracking = @import("testing/allocator_tracking.zig");

// Create tracking allocator
var tracker = tracking.TrackingAllocator.init(std.heap.page_allocator);
defer {
    const result = tracker.deinit();
    if (result == .leak) {
        std.log.err("Memory leak detected!", .{});
    }
}

const allocator = tracker.allocator();

// Use allocator normally...

// Check statistics
const stats = tracker.getStats();
std.log.info("Peak memory: {} bytes", .{stats.peak_bytes});
std.log.info("Total allocations: {}", .{stats.total_allocations});

Memory Metrics

Metric Description
total_allocations Number of allocation calls
total_frees Number of free calls
current_bytes Currently allocated bytes
peak_bytes Maximum allocated at any point
allocation_failures Failed allocation attempts

Troubleshooting

“perf: command not found”

Install perf tools for your distribution:

# Ubuntu/Debian
sudo apt install linux-perf

# If that doesn't work, try version-specific package
sudo apt install linux-tools-$(uname -r)

# Fedora
sudo dnf install perf

“[unknown]” Frames in Flame Graph

This should NOT happen with ArcherDB because frame pointers are preserved. If you see [unknown] frames:

  1. Verify build: Ensure you built with ./zig/zig build (frame pointers are enabled by default)
  2. Check binary: Run readelf -S zig-out/bin/archerdb | grep -i frame to verify
  3. Kernel symbols: For kernel stacks, ensure /proc/kallsyms is readable

If profiling third-party libraries:

# The library may need to be built with -fno-omit-frame-pointer
CFLAGS="-fno-omit-frame-pointer" ./configure && make

“Permission denied” or “Access denied”

Lower the perf_event_paranoid setting:

# Check current value
cat /proc/sys/kernel/perf_event_paranoid

# Values:
#  -1 = Allow all users
#   0 = Allow non-root but disallow kernel profiling
#   1 = Allow non-root with kernel profiling
#   2 = Disallow all non-root (default on many systems)
#   3 = Disallow all non-root completely

# Set to 1 (recommended)
sudo sysctl kernel.perf_event_paranoid=1

Alternatively, run with sudo:

sudo ./scripts/flamegraph.sh --output profile.svg -- ./zig-out/bin/archerdb benchmark

“No samples collected” or Empty Flame Graph

  1. Duration too short: Increase duration with -d 60
  2. Workload too fast: The command finished before sampling started
  3. Wrong frequency: Try lower frequency if samples are being throttled
# Check for throttling messages
dmesg | grep perf

# Use lower frequency
./scripts/flamegraph.sh --output profile.svg -f 49 -- ./zig-out/bin/archerdb benchmark

perf stat Shows “not supported”

Some hardware counters may not be available on your CPU:

# List available counters
perf list

# Use only universally available counters
./scripts/profile.sh -c cycles,instructions -- ./zig-out/bin/archerdb benchmark

Flame Graph SVG Won’t Open

  1. File too large: Reduce sampling duration or increase frequency
  2. Corrupted output: Check for error messages during generation
  3. Browser issues: Try a different browser (Chrome/Firefox work best)

Best Practices

When to Profile

  1. Before optimization: Establish baseline
  2. After optimization: Verify improvement
  3. Before release: Ensure no regressions
  4. After refactoring: Confirm performance preserved

Profiling Checklist

Common Pitfalls

  1. Debug builds: 10-100x slower, misleading profiles
  2. Small samples: High variance, unreliable results
  3. Cold caches: First run different from steady state
  4. Compiler optimizations: Release builds may inline/eliminate code
  5. System noise: Background processes affect measurements

Optimization Priority

Focus optimization efforts based on profiling data:

  1. Hot paths: Functions consuming >10% of CPU time
  2. Cache misses: High miss rate (>10%) in critical paths
  3. Branch mispredictions: High miss rate (>5%) in loops
  4. Memory allocations: Excessive allocations in hot paths

Tracy Real-Time Instrumentation

Tracy provides real-time instrumentation with visualization. ArcherDB supports Tracy in on-demand mode - zero overhead unless the Tracy profiler GUI is connected.

Building with Tracy

# Build with Tracy profiling enabled (on-demand mode)
./zig/zig build profile -Dtracy=true

# Or use the profiling flag
./zig/zig build -Dprofiling=true -Dtracy=true

Using Tracy Zones

ArcherDB provides ergonomic Tracy zone helpers that compile to no-ops when Tracy is disabled:

const tracy = @import("testing/tracy_zones.zig");

fn processQuery(query: Query) !Result {
    const zone = tracy.zone(@src(), "process_query");
    defer zone.end();

    // Add context to the zone
    zone.text(query.type);
    zone.value(query.entity_count);

    // ... processing ...
}

Available zone helpers:

Function Purpose
zone(@src(), "name") Create a named profiling zone
zoneN(@src(), name) Create zone with runtime name
frameMark() Mark frame boundary
message(text) Log to Tracy timeline
plot(name, value) Plot metric over time

Predefined Colors

Use semantic colors for different subsystems:

const colors = tracy.colors;

zone.color(colors.query);      // Green - query processing
zone.color(colors.storage);    // Blue - storage operations
zone.color(colors.consensus);  // Red - consensus/Raft
zone.color(colors.network);    // Yellow - network I/O
zone.color(colors.index);      // Magenta - index operations
zone.color(colors.geo);        // Orange - geo/S2 operations

Running Tracy

  1. Download Tracy profiler from https://github.com/wolfpld/tracy/releases
  2. Run the ArcherDB binary built with -Dtracy=true
  3. Connect Tracy profiler to the running process
  4. Zones will appear in the timeline when profiler connects

Tracy on-demand mode means the instrumentation has near-zero overhead when the profiler is not connected. Profiling only activates when the Tracy GUI establishes a connection.

Parca Continuous Profiling

Parca provides always-on continuous profiling using eBPF with <1% overhead. Ideal for production monitoring and historical analysis.

Prerequisites

  • Linux kernel >= 5.6 with eBPF support
  • Root privileges for eBPF programs

Quick Start

# Install Parca agent
sudo ./scripts/parca-agent.sh install

# Start with local Parca server
sudo ./scripts/parca-agent.sh start

# Check status
./scripts/parca-agent.sh status

Parca Server

Run a local Parca server for development:

# Using Docker
docker run -p 7070:7070 ghcr.io/parca-dev/parca:latest

# Or download binary
curl -sL https://github.com/parca-dev/parca/releases/latest/download/parca_Linux_x86_64.tar.gz | tar xz
./parca --config-path=parca.yaml

For production, consider Parca Cloud or self-hosted deployment.

Analyzing Profiles

  1. Open Parca UI at http://localhost:7070
  2. Select time range and process
  3. View flame graph of CPU usage over time
  4. Compare profiles between time periods to find regressions

Parca Features

  • Continuous profiling: Always-on with <1% overhead
  • Historical data: Query profiles from any point in time
  • Differential analysis: Compare profiles to find regressions
  • Label-based queries: Filter by process, container, node
  • eBPF-based: No code changes required

Profile Build Mode

ArcherDB provides a dedicated profile build mode optimized for profiling:

# Build with profiling support (frame pointers preserved)
./zig/zig build profile

# Build with Tracy instrumentation
./zig/zig build profile -Dtracy=true

The profile build:

  • Uses ReleaseFast optimization for representative performance
  • Preserves frame pointers for accurate stack traces
  • Outputs archerdb-profile binary

Additional Resources

Edit this page

Performance Tuning Guide

This guide helps you optimize ArcherDB for your specific workload. Use the checked-in benchmark artifacts and docs/BENCHMARKS.md for current measured results; this document focuses on tuning levers and validation workflow rather than fixed product numbers.

Quick Reference

Configuration Parameters

Parameter Default Optimized When to Change
ram_index_capacity 10K 500K High entity count (>10K active entities)
l0_compaction_trigger 4 8 Write-heavy workload (>50% writes)
compaction_threads 2 3 Compaction backlog (check metrics)
s2_covering_cache_size 512 2048 Spatial query heavy (>30% spatial queries)
s2_level_range 4 3 Need tighter spatial coverings
s2_min_level_adjustment 2 1 More precise S2 cell selection

Validation Goals

Metric Goal
Write throughput Validate against a comparable checked-in baseline
Read P99 <10ms on the target hardware profile
Radius query P99 <50ms on the target hardware profile
Polygon query P99 <100ms on the target hardware profile
Memory stability No leaks or unbounded growth

Key Optimizations

RAM Index Capacity

Problem: IndexDegraded errors when entity count exceeds default capacity.

Solution: Set ram_index_capacity to support your expected entity count at 50% load factor.

ram_index_capacity = expected_entities / 0.5

Example: For 250,000 active entities:

ram_index_capacity = 250,000 / 0.5 = 500,000

Why 50% load factor: Hash tables degrade rapidly above 70% utilization. 50% provides headroom for growth and maintains O(1) performance.

Memory impact:

Index memory = ram_index_capacity * 96 bytes
500K slots = 48 MB RAM-index budget

L0 Compaction Trigger

Problem: Write stalls when L0 fills up faster than compaction can process.

Solution: Increase l0_compaction_trigger from 4 to 8 for write-heavy workloads.

Trade-offs:

  • Higher (8-12): Fewer compaction cycles, better sustained write throughput, but more L0 files to check on reads
  • Lower (2-4): More aggressive compaction, fewer L0 files, but more write stalls during compaction

When to increase:

  • Write throughput is inconsistent (spikes and stalls)
  • archerdb_compaction_write_stalls metric is increasing
  • P99 write latency has high variance

Compaction Threads

Problem: Compaction backlog growing faster than single-threaded compaction can clear.

Solution: Increase compaction_threads from 2 to 3.

Trade-offs:

  • More threads = faster compaction but more CPU contention
  • Beyond 3-4 threads, diminishing returns due to I/O bottleneck

Monitoring: Watch archerdb_compaction_pending_bytes. If consistently growing, increase threads.

S2 Covering Cache

Problem: Repeated spatial queries recompute S2 cell coverings.

Solution: Increase s2_covering_cache_size from 512 to 2048 entries.

Impact: 4x better cache hit rate for repeated spatial query patterns (same delivery zones, geofences).

Memory impact: ~200 bytes per cache entry = ~400 KB for 2048 entries.

Workload-Specific Tuning

Write-Heavy Workloads (Fleet Tracking)

Characteristics: >70% writes, continuous position updates, high throughput requirements.

# Optimized for sustained writes
ram_index_capacity: 500000      # Support high entity count
l0_compaction_trigger: 8         # Delay compaction to reduce stalls
compaction_threads: 3            # Faster parallel compaction
partial_compaction: false        # Full compaction for better sustained throughput

Read-Heavy Workloads (Query Services)

Characteristics: >70% reads, spatial queries, result caching important.

# Optimized for read latency
s2_covering_cache_size: 4096     # Large cache for query patterns
s2_level_range: 3                # Tighter coverings, fewer false positives
l0_compaction_trigger: 4         # More aggressive compaction, fewer L0 files
grid_cache_size: 8GB             # Larger block cache

Mixed Workloads

Characteristics: 50/50 reads and writes, balanced requirements.

# Balanced defaults
ram_index_capacity: 250000
l0_compaction_trigger: 6
compaction_threads: 2
s2_covering_cache_size: 1024

Benchmarking

Running Benchmarks

Use maintained benchmark entry points for current measurements:

# Quick single-node smoke via the built-in benchmark driver
zig-out/bin/archerdb benchmark --event-count=100000 --query-uuid-count=10000 --query-radius-count=1000 --query-polygon-count=100

# Time-bounded multi-node harness run
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60

# Full release-style suite
python3 test_infrastructure/benchmarks/cli.py run --full-suite

The legacy ./scripts/benchmark_lsm.sh path now only forwards simple single-node write_only / read_only / mixed smoke runs to archerdb benchmark. It no longer fabricates benchmark numbers or supports time-bounded benchmark modes.

Interpreting Results

Key metrics to watch:

Write Performance:
  Throughput: compare against the latest checked-in baseline on comparable hardware
  P99 Latency: keep tail latency stable while ingest volume increases

Read Performance:
  UUID Query P99: validate against your target SLA and checked-in evidence
  Radius Query P99: validate against your target SLA and checked-in evidence
  Polygon Query P99: validate against your target SLA and checked-in evidence

Stability:
  Memory Growth: 0 MB/hour (no leaks)
  Throughput CV: < 10% variance on repeated runs

Hardware Scaling

Absolute throughput depends primarily on CPU, memory bandwidth, storage, and topology. Treat checked-in benchmark artifacts as evidence for a specific machine/profile, not as universal product guarantees.

Monitoring for Performance

Key Metrics

Metric Warning Critical Action
archerdb_request_duration_p99{type="insert"} >25ms >100ms Check compaction, disk I/O
archerdb_request_duration_p99{type="radius"} >50ms >200ms Increase S2 cache
archerdb_compaction_pending_bytes >1GB >5GB Increase compaction threads
archerdb_index_load_factor >0.6 >0.75 Increase RAM index capacity
archerdb_lsm_l0_files >8 >16 Increase L0 trigger or threads

Grafana Dashboard

The ArcherDB Grafana dashboard (deploy/grafana/dashboards/archerdb-overview.json) includes performance panels:

  • Throughput: Insert/query rates over time
  • Latency: P50, P99, P999 histograms
  • Compaction: Pending bytes, write amplification
  • Index: Load factor, tombstone ratio

Alert Rules

Performance-related alerts in deploy/prometheus/rules.yaml:

# High latency alert
- alert: ArcherDBHighLatency
  expr: archerdb_request_duration_p99 > 0.1  # 100ms
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "P99 latency exceeds 100ms"
    runbook_url: "docs/performance-tuning.md"

# Index degraded alert
- alert: ArcherDBIndexDegraded
  expr: archerdb_index_load_factor > 0.75
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "RAM index at critical capacity"
    action: "Increase ram_index_capacity or scale horizontally"

Troubleshooting Performance Issues

Symptom: Write Stalls

Diagnosis:

curl -s localhost:9090/metrics | grep archerdb_compaction

If compaction_pending_bytes is high:

  1. Increase compaction_threads to 3-4
  2. Increase l0_compaction_trigger to 8-12
  3. Check disk I/O with iostat -x 1 5

Symptom: High Read Latency

Diagnosis:

curl -s localhost:9090/metrics | grep -E "(lsm_l0_files|cache_hit)"

If lsm_l0_files is high:

  • Compaction is behind; increase threads

If cache hit rate is low:

  • Increase grid_cache_size or s2_covering_cache_size

Symptom: IndexDegraded Alert

Diagnosis:

curl -s localhost:9090/metrics | grep archerdb_index_load_factor

If load factor > 0.7:

  1. Increase ram_index_capacity (requires restart)
  2. Or scale horizontally (add shards)
  3. Or reduce entity count (TTL, archival)
Edit this page

LSM Tree Tuning Guide

This guide describes ArcherDB’s LSM tuning model after the tier redesign.

All tier presets now share one high-performance runtime profile. Tier differences exist only in capacity quotas (RAM index and disk limits).

Runtime Model

ArcherDB uses one shared runtime profile for lite, standard, pro, enterprise, and ultra.

Runtime parameter Shared value Why
message_size_max 10 MiB Prevents request-size bottlenecks in ingest/query paths
block_size 1 MiB Maximizes sequential I/O throughput
lsm_levels 8 Large capacity envelope with predictable compaction behavior
lsm_growth_factor 8 Balanced write/read amplification
lsm_compaction_ops 128 Larger memtables, fewer flush cycles
lsm_manifest_compact_extra_blocks 3 Keeps manifest growth bounded
lsm_table_coalescing_threshold_percent 35 Aggressive coalescing for space efficiency
pipeline_prepare_queue_max 24 Higher parallelism in the prepare pipeline
journal_slot_count 1024 Keeps WAL sizing practical while preserving throughput
journal_iops_write_max 32 Matches journal safety invariants with current WAL layout
journal_iops_read_max 24 High read-side WAL concurrency
grid_iops_read_max 96 High read concurrency for LSM/grid access
grid_iops_write_max 96 High write concurrency for compaction/replication work

Capacity-Only Tier Matrix

These are the only intended differences between tier profiles:

Tier RAM index default Storage default / max
lite 128 MiB 4 GiB / 4 GiB
standard 4 GiB 64 GiB / 256 GiB
pro 16 GiB 512 GiB / 2 TiB
enterprise 32 GiB 4 TiB / 16 TiB
ultra 64 GiB 16 TiB / 64 TiB

What “Capacity-Only” Means in Practice

  • If ingest stops because of TOO_MUCH_DATA (status=1) before RAM or disk is exhausted, that is a transport/request-shape issue, not a tier capacity limit.
  • Correct capacity failures should present as resource boundaries, for example:
    • RAM index pressure (IndexDegraded)
    • Storage size limit exhaustion
  • Tier selection should not be used to tune throughput or latency behavior.

Hardware Guidance

Runtime tuning is shared, so hardware determines absolute throughput:

Target throughput Suggested CPU Suggested RAM Suggested storage
100K events/sec 8 cores 16+ GB NVMe Gen3+
500K events/sec 16 cores 32+ GB Fast NVMe
1M+ events/sec 32+ cores 64+ GB NVMe Gen4/Gen5

Use tier quotas for capacity governance, not performance throttling.

Benchmarking

Quick local smoke

zig-out/bin/archerdb benchmark --event-count=100000 --query-uuid-count=10000 --query-radius-count=1000 --query-polygon-count=100

Maintained benchmark harness

python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60

Capacity test (real run)

python3 scripts/test_capacity_limits.py --config lite --optimize ReleaseFast
python3 scripts/test_capacity_limits.py --config standard --optimize ReleaseFast

By default, capacity test artifacts are written to /tmp/archerdb_capacity_runs.

Interpreting Results

For capacity runs, verify:

  1. No early transport bottleneck (status=1) at normal batch sizes.
  2. Throughput remains in the same order of magnitude across tiers on the same hardware.
  3. Failure reason changes with capacity quotas, not tier runtime behavior.

Example summary fields:

  • events_inserted
  • unique_entries
  • cpu_percent_avg / cpu_percent_peak
  • ram_rss_avg_bytes / ram_rss_peak_bytes
  • disk_logical_bytes / disk_physical_bytes_from_du
  • failure_reason and first_error_code

Troubleshooting

Capacity run fails with status=1

  • Reduce per-request payload in the test runner (adaptive batch backoff should already do this).
  • Ensure the test uses the intended tier build and optimize mode.
  • Confirm server request envelope has not been overridden to a small value.

Capacity run fails too early on RAM index

  • This is expected for lower-capacity tiers.
  • --ram-index-size can lower RAM index budget at runtime but cannot exceed the tier cap.
  • Increase ram_index_size_default only if product intent requires a higher capacity boundary.

Capacity run fails on storage limit

  • Verify storage_size_limit_default and storage_size_limit_max for the selected tier.
  • Use larger tier quotas when the workload requires longer retention.

References

  • src/config.zig
  • src/constants.zig
  • scripts/test_capacity_limits.py
  • zig-out/bin/archerdb benchmark
  • test_infrastructure/benchmarks/cli.py
Edit this page

ArcherDB Testing Guide

Comprehensive guide for running ArcherDB tests locally across all 5 SDKs.

Overview

ArcherDB’s test suite covers:

  • Unit tests: Per-SDK operation validation
  • Integration tests: Multi-node cluster behavior
  • Parity tests: Cross-SDK result consistency
  • Edge case tests: Geographic boundary conditions
  • Performance tests: Latency and throughput benchmarks

Prerequisites

Required Software

Dependency Version Purpose
Python 3.11+ Test infrastructure, Python SDK
Node.js 20+ Node.js SDK tests
Go 1.21+ Go SDK tests
Java 21+ Java SDK tests (Maven included)
GCC/Clang Recent C SDK tests
Zig Bundled Core build and server tests

Installation

# Python test infrastructure
pip install -r test_infrastructure/requirements.txt

# Node.js SDK dependencies
cd src/clients/node && npm install

# Go SDK dependencies
cd src/clients/go && go mod download

# Java SDK dependencies
cd src/clients/java && mvn dependency:resolve

# C SDK - no external dependencies (header-only)
# Zig - bundled in repo at ./zig/zig (for server build/tests)

Quick Start

1. Build the Server

# Constrained build (recommended for most machines)
./zig/zig build -j4 -Dconfig=lite

# Full build (CI or dedicated machine)
./zig/zig build

2. Start a Local Server

# Single node for development
./zig/zig build run -- --port 3001

# Or run the pre-built binary
./zig-out/bin/archerdb --port 3001

3. Run SDK Tests

Each SDK has its own test suite. Run from the repository root:

Python:

cd src/clients/python
pip install pytest
pytest tests/ -v

Node.js:

cd src/clients/node
npm install
npm test

Go:

cd src/clients/go
go test ./... -v

Java:

cd src/clients/java
mvn test

C:

cd src/clients/c
make test

Server (Zig unit tests):

./zig/zig build -j4 -Dconfig=lite test:unit

4. Run Specific Test Filter

Most test frameworks support filtering:

# Python
pytest tests/ -v -k "insert"

# Go
go test ./... -v -run TestInsert

# Server (Zig)
./zig/zig build -j4 -Dconfig=lite test:unit -- --test-filter "insert"

Test Infrastructure

The test_infrastructure/ directory provides Python utilities for cluster management and test data generation.

Cluster Harness

Start and manage multi-node ArcherDB clusters programmatically:

from test_infrastructure.harness import ArcherDBCluster, ClusterConfig

# Start a 3-node cluster
config = ClusterConfig(node_count=3)
with ArcherDBCluster(config) as cluster:
    cluster.wait_for_ready(timeout=60)
    leader_addr = cluster.get_leader_address()
    # Run tests against leader_addr...

Data Generators

Generate test datasets with various distribution patterns:

from test_infrastructure.generators import generate_events, DatasetConfig

# Generate 1000 events concentrated around cities
events = generate_events(DatasetConfig(
    size=1000,
    pattern='city_concentrated',
    cities=['san_francisco', 'tokyo'],
    seed=42,  # Reproducible
))

See test_infrastructure/README.md for complete documentation.

Fixtures

Pre-defined test fixtures are in test_infrastructure/fixtures/v1/:

Fixture Size Use Case
smoke.json 10 events Quick connectivity tests
pr.json 100 events PR validation
nightly.json 1000 events Comprehensive testing

Environment Variables

Variable Description Default
ARCHERDB_HOST Server hostname 127.0.0.1
ARCHERDB_PORT Server port 3001
ARCHERDB_INTEGRATION Enable integration tests "" (disabled)
PRESERVE_ON_FAILURE Keep cluster data after failures "" (cleanup)
ARCHERDB_BIN Path to archerdb binary Auto-detect

Running Integration Tests

Integration tests require a running cluster and are gated by environment variable:

# Start cluster first
./zig/zig build run -- --port 3001

# Enable integration tests
export ARCHERDB_INTEGRATION=1
pytest tests/ -v -m integration

Parity Testing

Verify that all SDKs produce identical results:

# Run full parity suite
python tests/parity_tests/parity_runner.py

# Run specific operations
python tests/parity_tests/parity_runner.py --ops insert query-radius

# Run specific SDKs
python tests/parity_tests/parity_runner.py --sdks python node go

# Verbose output
python tests/parity_tests/parity_runner.py -v

Results are written to:

  • reports/parity.json - Machine-readable
  • docs/PARITY.md - Human-readable matrix

See docs/PARITY.md for methodology and current status.

Edge Case Testing

Geographic edge cases (poles, antimeridian, equator) are tested separately:

# Run edge case tests
pytest tests/edge_case_tests/ -v

# Run specific category
pytest tests/edge_case_tests/ -v -k "polar"
pytest tests/edge_case_tests/ -v -k "antimeridian"

Troubleshooting

Server Won’t Start

  1. Check if binary exists:

    ls zig-out/bin/archerdb
  2. Build if missing:

    ./zig/zig build -j4 -Dconfig=lite
  3. Check for port conflicts:

    lsof -i :3001

Tests Fail with Connection Errors

  1. Verify server is running:

    curl http://127.0.0.1:3001/ping
    # Should return: {"pong":true}
  2. Check environment variables:

    echo $ARCHERDB_HOST $ARCHERDB_PORT

Python Import Errors

Ensure test infrastructure is in path:

export PYTHONPATH="${PYTHONPATH}:${PWD}/test_infrastructure"

Or install in development mode:

pip install -e test_infrastructure/

Out of Memory During Tests

Use constrained build configuration:

# Instead of full build
./zig/zig build -j4 -Dconfig=lite test:unit

# Or minimal for low-memory systems
./zig/zig build -j2 -Dconfig=lite test:unit

Preserving Test Data for Debugging

export PRESERVE_ON_FAILURE=1
pytest tests/
# Data preserved in /tmp/archerdb-test-*

Resource-Constrained Testing

For machines with limited resources (24GB RAM, 8 cores):

Profile Command RAM Use Case
Minimal -j2 -Dconfig=lite ~2GB Heavy server load
Constrained -j4 -Dconfig=lite ~4GB Normal development
Full (default) ~8GB+ CI or dedicated machine

Use the helper script:

./scripts/test-constrained.sh unit              # Default: -j4, lite
./scripts/test-constrained.sh --minimal unit    # Minimal: -j2, lite
./scripts/test-constrained.sh --full unit       # Full resources
./scripts/test-constrained.sh check             # Quick compile check

CI Integration

Tests are run automatically in CI with tiered execution:

  • Smoke (<5 min): Every push, basic connectivity
  • PR (<15 min): Pull requests, full SDK suite
  • Nightly (2h): Manual-dispatch comprehensive multi-node testing
  • Weekly (3h): Manual-dispatch benchmark publication

See docs/testing/ci-tiers.md for tier details.

See Also


Last updated: 2026-02-01

Edit this page

CI Tier Structure

ArcherDB uses a tiered CI approach to balance fast feedback with comprehensive testing.

Overview

Tier Duration Trigger Purpose
Smoke <5 min Every push Fast feedback, gate PRs
PR <15 min Pull requests Comprehensive validation
Nightly 2h Manual dispatch Full coverage, edge cases
Weekly 3h Manual dispatch Performance regression detection and history publication

Tier 1: Smoke Tests

Trigger: Every push to main, every commit in PRs

Duration: <5 minutes

Scope:

  • Build verification (compiles cleanly)
  • Basic connectivity test per SDK
  • Single operation per SDK (insert + query)
  • Single-node topology only

Purpose:

  • Immediate feedback on breakage
  • Gate for PR merges
  • Catch obvious regressions fast

Failure handling:

  • Blocks PR merge
  • Notifies author immediately
  • Must fix before proceeding

Jobs:

Job Tests Time
build Compile check 30s
python-smoke 3 tests 45s
node-smoke 3 tests 45s
go-smoke 3 tests 30s
java-smoke 3 tests 90s
c-smoke 3 tests 20s

All SDK jobs run in parallel using GitHub Actions matrix strategy.

Tier 2: PR Tests

Trigger: Pull request opened, synchronized, or reopened

Duration: <15 minutes

Scope:

  • Full SDK test suite for all 5 SDKs
  • Single-node topology
  • All 14 operations tested
  • Error handling verification
  • Retry logic validation

Purpose:

  • Comprehensive validation before merge
  • Catch edge cases smoke tests miss
  • Verify parity across SDKs

Failure handling:

  • Blocks PR merge
  • Detailed test report in PR comment
  • Must fix all failures before merge

Jobs:

Job Tests Time
python-full ~100 tests 3 min
node-full ~80 tests 2 min
go-full ~70 tests 1.5 min
java-full ~60 tests 4 min
c-full ~50 tests 1 min
zig-full ~50 tests 1 min
parity-check 84 cells 3 min

Tier 3: Nightly Tests

Trigger: Manual workflow_dispatch

Duration: ~2 hours

Scope:

  • All Tier 2 tests plus:
  • Multi-node topologies (1, 3, 5, 6 nodes)
  • Geographic edge cases (poles, antimeridian, equator)
  • Failure injection tests
  • Recovery verification
  • Long-running stability tests

Purpose:

  • Catch topology-specific issues
  • Verify multi-node consistency
  • Test failure recovery paths
  • Find rare race conditions

Failure handling:

  • Does NOT block merges
  • Used for release-candidate and deep validation runs
  • Failures are triaged manually from workflow output and artifacts

Jobs:

Job Description Time
topology-1 Single node, all SDKs 15 min
topology-3 3-node cluster, all SDKs 25 min
topology-5 5-node cluster, all SDKs 30 min
topology-6 6-node cluster, all SDKs 35 min
edge-cases Geographic boundaries 10 min
failure-injection Node failures, partitions 20 min
stability Long-running workload 15 min

Tier 4: Weekly Benchmarks

Trigger: Manual workflow_dispatch

Duration: ~3 hours

Scope:

  • Full benchmark suite across topologies
  • Throughput benchmarks (events/sec)
  • Read latency (P50, P95, P99)
  • Write latency (P50, P95, P99)
  • Mixed workload benchmarks
  • SDK parity benchmarks

Purpose:

  • Detect performance regressions
  • Track performance trends
  • Compare SDK performance
  • Validate scaling behavior

Failure handling:

  • Used when maintainers want to refresh published benchmark history
  • Results are reviewed manually before promotion into long-term history
  • Does not block future merges

Performance Targets:

Metric Target Alert Threshold
3-node throughput >=770K events/sec -10%
Read latency P95 <1ms +20%
Read latency P99 <10ms +20%
Write latency P95 <10ms +20%
Write latency P99 <50ms +20%

Regression Detection:

  • Uses Welch’s t-test for statistical significance
  • Compares against stored baseline (JSON)
  • 95% confidence level (alpha=0.05)
  • Requires consistent CV <10% before comparison

Jobs:

Job Description Time
benchmark-1 Single node 30 min
benchmark-3 3-node cluster 45 min
benchmark-5 5-node cluster 50 min
benchmark-6 6-node cluster 55 min
sdk-parity-bench SDK comparison 20 min

Local benchmark outputs live under reports/benchmarks/, reports/history/, and reports/baselines/. Published history can be promoted into benchmarks/history/YYYY-MM-DD.json by the manual benchmark publication workflow.

Hardware

Tier Runner Specs
Smoke ubuntu-latest 2 cores, 7GB RAM
PR ubuntu-latest 2 cores, 7GB RAM
Nightly ubuntu-latest-4-cores 4 cores, 16GB RAM
Weekly ubuntu-latest-8-cores 8 cores, 32GB RAM

Artifacts

All tiers upload artifacts for debugging:

Artifact Retention Contents
test-reports 14 days JUnit XML, pytest output
coverage 14 days Coverage HTML, lcov data
logs 7 days Server logs, stderr
benchmarks 90 days JSON results, CSV data

Running Locally

Simulate each tier locally:

Smoke:

./scripts/test-constrained.sh check
pytest tests/ -v -m smoke --timeout=60

PR:

./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300

Nightly (requires cluster setup):

export ARCHERDB_INTEGRATION=1
python -m test_infrastructure.harness.cli start --nodes=3
pytest tests/ -v -m "integration or nightly"
python -m test_infrastructure.harness.cli stop

Weekly (requires cluster setup):

python3 test_infrastructure/benchmarks/cli.py run --full-suite

Workflow Files

CI workflows are defined in .github/workflows/:

File Tier
sdk-smoke.yml Smoke tests
sdk-pr.yml PR tests
sdk-nightly.yml Nightly tests
benchmark-weekly.yml Benchmark publication

Monitoring

CI health dashboard: Track test stability, flakiness, and duration trends.

Key metrics:

  • Pass rate by tier (target: >99% for smoke/PR)
  • Mean duration by tier
  • Flaky test count (target: 0)
  • Regression frequency (weekly benchmark)

See Also


Last updated: 2026-02-01

Edit this page

Performance Baseline Management

This document describes how ArcherDB manages performance baselines for regression detection in CI.

Overview

Performance baselines are locked reference points that CI uses to detect regressions. Each PR’s benchmark results are compared against the current main branch baseline. Regressions block merge to prevent shipping slow code.

Regression Thresholds

The following thresholds are used to detect performance regressions:

Metric Threshold Rationale
Throughput 5% Matches observed 5% coefficient of variation (CV) in benchmarks
Latency P99 25% Accounts for higher variance in tail latencies

Throughput: If current mean execution time is >5% slower than baseline, the check fails.

  • Formula: current_mean > baseline_mean * 1.05
  • Example: Baseline 1000ns, current 1060ns = 6% slower = FAIL

Latency P99: If current P99 latency is >25% higher than baseline, the check fails.

  • Formula: current_p99 > baseline_p99 * 1.25
  • Example: Baseline P99 1200ns, current P99 1400ns = 16.7% higher = PASS
  • Example: Baseline P99 1200ns, current P99 1600ns = 33.3% higher = FAIL

Baseline Lifecycle

main branch push
       |
       v
+------------------+
| Run benchmarks   |
| (full mode)      |
+------------------+
       |
       v
+------------------+
| Upload as        |
| benchmark-       |
| baseline         |
+------------------+
       |
       v
  (90 day retention)
PR created/updated
       |
       v
+------------------+
| Download         |
| baseline from    |
| main             |
+------------------+
       |
       v
+------------------+
| Run benchmarks   |
| (quick mode)     |
+------------------+
       |
       v
+------------------+
| Compare against  |
| baseline         |
+------------------+
       |
    +--+--+
    |     |
 PASS   FAIL
    |     |
    v     v
 Merge  Block
 OK     merge

Timeline

  1. Main branch pushes upload new baseline artifact (full benchmark mode)
  2. PRs download the current main baseline and run quick benchmarks
  3. Comparison checks both throughput and P99 latency against thresholds
  4. Regressions block merge until fixed or baseline is reset

Resetting the Baseline

Sometimes you need to reset the baseline after intentional performance changes:

When to Reset

  • Intentional trade-off: You accepted slower writes for better consistency
  • New feature overhead: Added necessary functionality that increases latency
  • Algorithm change: Changed from O(n) to O(log n) with different constants

How to Reset

  1. Delete the current baseline artifact:

    • Go to GitHub Actions > Select a recent main workflow run
    • Find “benchmark-baseline” artifact and delete it
    • Or use GitHub CLI: gh api -X DELETE /repos/{owner}/{repo}/actions/artifacts/{artifact_id}
  2. Merge your PR (no comparison runs without baseline)

  3. New baseline created on next main push

Alternative: Update Baseline Manually

If you don’t want to delete the artifact:

  1. Merge to main (workflow runs)
  2. New baseline automatically uploaded
  3. Future PRs compare against new baseline

Troubleshooting

False Positives

Symptom: Benchmark fails but code hasn’t changed performance.

Possible causes:

  • Stale baseline: If baseline is very old, machine differences may cause variance
  • CI runner variance: GitHub Actions runners can have different performance
  • Background load: Other jobs running on same machine

Resolution:

  • Re-run the benchmark job
  • If consistent, reset the baseline

Consistent Failures

Symptom: Multiple re-runs show same regression.

This likely indicates a real regression:

  1. Review recent commits for performance-impacting changes
  2. Profile locally to identify hot paths
  3. Fix the performance issue
  4. If intentional, reset the baseline and document why

No Baseline Available

Symptom: “Download baseline” step shows “Artifact not found”.

This is normal for:

  • First run after repository setup
  • After baseline was manually deleted
  • Baseline artifact expired (90 day retention)

Resolution: Merge to main to create new baseline.

jq/bc Not Available

Symptom: “jq not installed” warning in comparison output.

Resolution: The workflow installs these dependencies. If you’re running locally, install them:

# Ubuntu/Debian
sudo apt-get install jq bc

# macOS
brew install jq bc

Configuration

Modifying Thresholds

Thresholds are defined in scripts/benchmark-ci.sh:

# Throughput: 5% threshold
throughput_threshold=$(echo "scale=0; $baseline_mean * 1.05 / 1" | bc)

# Latency P99: 25% threshold
latency_threshold=$(echo "scale=0; $baseline_p99 * 1.25 / 1" | bc)

To change thresholds:

  1. Edit the multipliers in benchmark-ci.sh
  2. Update this documentation
  3. Update workflow header comments

Benchmark Modes

Mode Duration Use Case
quick ~30 seconds PRs (fast feedback)
full ~5 minutes Main branch (accurate baseline)
  • .github/workflows/benchmark.yml - CI workflow
  • scripts/benchmark-ci.sh - Benchmark runner and comparison logic
  • .planning/phases/09-testing-infrastructure/09-CONTEXT.md - Threshold decisions

References

  • .planning/phases/09-testing-infrastructure/09-CONTEXT.md - Why these thresholds were chosen
  • .planning/STATE.md - Observed 5% CV in benchmarks
Edit this page

CI Tier Structure

ArcherDB uses a tiered CI approach to balance fast feedback with comprehensive testing.

Overview

Tier Duration Trigger Purpose
Smoke <5 min Every push Fast feedback, gate PRs
PR <15 min Pull requests Comprehensive validation
Nightly 2h Manual dispatch Full coverage, edge cases
Weekly 3h Manual dispatch Performance regression detection and history publication

Tier 1: Smoke Tests

Trigger: Every push to main, every commit in PRs

Duration: <5 minutes

Scope:

  • Build verification (compiles cleanly)
  • Basic connectivity test per SDK
  • Single operation per SDK (insert + query)
  • Single-node topology only

Purpose:

  • Immediate feedback on breakage
  • Gate for PR merges
  • Catch obvious regressions fast

Failure handling:

  • Blocks PR merge
  • Notifies author immediately
  • Must fix before proceeding

Jobs:

Job Tests Time
build Compile check 30s
python-smoke 3 tests 45s
node-smoke 3 tests 45s
go-smoke 3 tests 30s
java-smoke 3 tests 90s
c-smoke 3 tests 20s

All SDK jobs run in parallel using GitHub Actions matrix strategy.

Tier 2: PR Tests

Trigger: Pull request opened, synchronized, or reopened

Duration: <15 minutes

Scope:

  • Full SDK test suite for all 5 SDKs
  • Single-node topology
  • All 14 operations tested
  • Error handling verification
  • Retry logic validation

Purpose:

  • Comprehensive validation before merge
  • Catch edge cases smoke tests miss
  • Verify parity across SDKs

Failure handling:

  • Blocks PR merge
  • Detailed test report in PR comment
  • Must fix all failures before merge

Jobs:

Job Tests Time
python-full ~100 tests 3 min
node-full ~80 tests 2 min
go-full ~70 tests 1.5 min
java-full ~60 tests 4 min
c-full ~50 tests 1 min
zig-full ~50 tests 1 min
parity-check 84 cells 3 min

Tier 3: Nightly Tests

Trigger: Manual workflow_dispatch

Duration: ~2 hours

Scope:

  • All Tier 2 tests plus:
  • Multi-node topologies (1, 3, 5, 6 nodes)
  • Geographic edge cases (poles, antimeridian, equator)
  • Failure injection tests
  • Recovery verification
  • Long-running stability tests

Purpose:

  • Catch topology-specific issues
  • Verify multi-node consistency
  • Test failure recovery paths
  • Find rare race conditions

Failure handling:

  • Does NOT block merges
  • Used for release-candidate and deep validation runs
  • Failures are triaged manually from workflow output and artifacts

Jobs:

Job Description Time
topology-1 Single node, all SDKs 15 min
topology-3 3-node cluster, all SDKs 25 min
topology-5 5-node cluster, all SDKs 30 min
topology-6 6-node cluster, all SDKs 35 min
edge-cases Geographic boundaries 10 min
failure-injection Node failures, partitions 20 min
stability Long-running workload 15 min

Tier 4: Weekly Benchmarks

Trigger: Manual workflow_dispatch

Duration: ~3 hours

Scope:

  • Full benchmark suite across topologies
  • Throughput benchmarks (events/sec)
  • Read latency (P50, P95, P99)
  • Write latency (P50, P95, P99)
  • Mixed workload benchmarks
  • SDK parity benchmarks

Purpose:

  • Detect performance regressions
  • Track performance trends
  • Compare SDK performance
  • Validate scaling behavior

Failure handling:

  • Used when maintainers want to refresh published benchmark history
  • Results are reviewed manually before promotion into long-term history
  • Does not block future merges

Performance Targets:

Metric Target Alert Threshold
3-node throughput >=770K events/sec -10%
Read latency P95 <1ms +20%
Read latency P99 <10ms +20%
Write latency P95 <10ms +20%
Write latency P99 <50ms +20%

Regression Detection:

  • Uses Welch’s t-test for statistical significance
  • Compares against stored baseline (JSON)
  • 95% confidence level (alpha=0.05)
  • Requires consistent CV <10% before comparison

Jobs:

Job Description Time
benchmark-1 Single node 30 min
benchmark-3 3-node cluster 45 min
benchmark-5 5-node cluster 50 min
benchmark-6 6-node cluster 55 min
sdk-parity-bench SDK comparison 20 min

Local benchmark outputs live under reports/benchmarks/, reports/history/, and reports/baselines/. Published history can be promoted into benchmarks/history/YYYY-MM-DD.json by the manual benchmark publication workflow.

Hardware

Tier Runner Specs
Smoke ubuntu-latest 2 cores, 7GB RAM
PR ubuntu-latest 2 cores, 7GB RAM
Nightly ubuntu-latest-4-cores 4 cores, 16GB RAM
Weekly ubuntu-latest-8-cores 8 cores, 32GB RAM

Artifacts

All tiers upload artifacts for debugging:

Artifact Retention Contents
test-reports 14 days JUnit XML, pytest output
coverage 14 days Coverage HTML, lcov data
logs 7 days Server logs, stderr
benchmarks 90 days JSON results, CSV data

Running Locally

Simulate each tier locally:

Smoke:

./scripts/test-constrained.sh check
pytest tests/ -v -m smoke --timeout=60

PR:

./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300

Nightly (requires cluster setup):

export ARCHERDB_INTEGRATION=1
python -m test_infrastructure.harness.cli start --nodes=3
pytest tests/ -v -m "integration or nightly"
python -m test_infrastructure.harness.cli stop

Weekly (requires cluster setup):

python3 test_infrastructure/benchmarks/cli.py run --full-suite

Workflow Files

CI workflows are defined in .github/workflows/:

File Tier
sdk-smoke.yml Smoke tests
sdk-pr.yml PR tests
sdk-nightly.yml Nightly tests
benchmark-weekly.yml Benchmark publication

Monitoring

CI health dashboard: Track test stability, flakiness, and duration trends.

Key metrics:

  • Pass rate by tier (target: >99% for smoke/PR)
  • Mean duration by tier
  • Flaky test count (target: 0)
  • Regression frequency (weekly benchmark)

See Also


Last updated: 2026-02-01

Edit this page

ArcherDB curl Examples

Complete curl examples for all 14 ArcherDB operations. All JSON is minified for easy copy-paste.

Prerequisites

Start a local ArcherDB server before running these examples:

# Single node (development)
./zig/zig build run -- --port 3001

# Or run the pre-built binary
./archerdb --port 3001

# Or using Docker
docker run -p 3001:3001 archerdb/archerdb

Quick Health Check

curl http://localhost:3001/ping
# Response: {"pong":true}

Insert Operations

1. Insert Single Event

Insert a single geo event with minimal required fields.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"1001","lat_nano":37774900000,"lon_nano":-122419400000}]}'

Expected response:

{"results":[{"index":0,"code":0}],"committed":true}

Insert with All Fields

Insert with all optional fields populated.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"2001","lat_nano":37774900000,"lon_nano":-122419400000,"group_id":1,"ttl_seconds":3600,"altitude_mm":100000,"velocity_mms":15000,"accuracy_mm":5000,"heading_cdeg":9000,"correlation_id":"11111","user_data":"42","flags":4}]}'

Insert Batch (Multiple Events)

Insert 3 events in a single batch.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"3001","lat_nano":40712800000,"lon_nano":-74006000000},{"entity_id":"3002","lat_nano":40712900000,"lon_nano":-74006100000},{"entity_id":"3003","lat_nano":40713000000,"lon_nano":-74006200000}]}'

Insert Error: Invalid Latitude

Latitude must be in range -90 to +90 degrees (i.e., -90e9 to +90e9 nanodegrees).

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"1001","lat_nano":100000000000,"lon_nano":0}]}'

Expected response:

{"results":[{"index":0,"code":9,"message":"LAT_OUT_OF_RANGE"}],"committed":false}

Insert Error: Invalid Longitude

Longitude must be in range -180 to +180 degrees.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"1001","lat_nano":0,"lon_nano":200000000000}]}'

Expected response:

{"results":[{"index":0,"code":10,"message":"LON_OUT_OF_RANGE"}],"committed":false}

Insert Error: Zero Entity ID

Entity ID cannot be zero.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"0","lat_nano":40712800000,"lon_nano":-74006000000}]}'

Expected response:

{"results":[{"index":0,"code":7,"message":"ENTITY_ID_MUST_NOT_BE_ZERO"}],"committed":false}

Upsert Operations

2. Upsert Event

Insert or update an event. Recommended for idempotent operations.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"1001","lat_nano":37774900000,"lon_nano":-122419400000}],"mode":"upsert"}'

Expected response (new entity):

{"results":[{"index":0,"code":0,"updated":false}],"committed":true}

Expected response (existing entity updated):

{"results":[{"index":0,"code":0,"updated":true}],"committed":true}

Upsert with Updated Location

Update an existing entity’s location.

curl -X POST http://localhost:3001/events -H "Content-Type: application/json" -d '{"events":[{"entity_id":"1001","lat_nano":37775000000,"lon_nano":-122420000000,"group_id":1}],"mode":"upsert"}'

Delete Operations

3. Delete Entity

Delete all data for a single entity.

curl -X DELETE http://localhost:3001/entities -H "Content-Type: application/json" -d '{"entity_ids":["1001"]}'

Expected response:

{"deleted_count":1,"not_found_count":0}

Delete Multiple Entities

Delete multiple entities in one request.

curl -X DELETE http://localhost:3001/entities -H "Content-Type: application/json" -d '{"entity_ids":["1001","1002","1003"]}'

Delete Non-existent Entity

Deleting an entity that doesn’t exist is not an error.

curl -X DELETE http://localhost:3001/entities -H "Content-Type: application/json" -d '{"entity_ids":["9999999"]}'

Expected response:

{"deleted_count":0,"not_found_count":1}

Query Operations

4. Query by UUID (Get Latest)

Get the most recent location for a single entity.

curl http://localhost:3001/entity/1001

Expected response (found):

{"event":{"entity_id":"1001","lat_nano":37774900000,"lon_nano":-122419400000,"timestamp_ns":1706745600000000000},"found":true}

Expected response (not found):

{"event":null,"found":false}

Query with UUID Format

Entity IDs can be in UUID format.

curl http://localhost:3001/entity/550e8400-e29b-41d4-a716-446655440000

5. Query UUID Batch

Get latest locations for multiple entities in one request.

curl -X POST http://localhost:3001/entities/batch -H "Content-Type: application/json" -d '{"entity_ids":["1001","1002","1003"]}'

Expected response:

{"events":[{"entity_id":"1001","lat_nano":37774900000,"lon_nano":-122419400000},null,{"entity_id":"1003","lat_nano":40712800000,"lon_nano":-74006000000}]}

6. Query Radius

Find all entities within 1km of a point.

curl -X POST http://localhost:3001/query/radius -H "Content-Type: application/json" -d '{"center_lat_nano":37774900000,"center_lon_nano":-122419400000,"radius_mm":1000000,"limit":100}'

Note: radius_mm is in millimeters. 1,000,000 mm = 1 km.

Expected response:

{"events":[{"entity_id":"1001","lat_nano":37774500000,"lon_nano":-122419000000,"distance_mm":50000}],"has_more":false,"cursor":null}

Query Radius with Group Filter

Filter results by group ID.

curl -X POST http://localhost:3001/query/radius -H "Content-Type: application/json" -d '{"center_lat_nano":37774900000,"center_lon_nano":-122419400000,"radius_mm":5000000,"group_id":1,"limit":100}'

Query Radius - Empty Result

Query returns empty array when no entities match.

curl -X POST http://localhost:3001/query/radius -H "Content-Type: application/json" -d '{"center_lat_nano":0,"center_lon_nano":0,"radius_mm":100000,"limit":100}'

Expected response:

{"events":[],"has_more":false,"cursor":null}

7. Query Polygon

Find all entities within a rectangular area.

curl -X POST http://localhost:3001/query/polygon -H "Content-Type: application/json" -d '{"vertices":[{"lat_nano":37790000000,"lon_nano":-122420000000},{"lat_nano":37790000000,"lon_nano":-122390000000},{"lat_nano":37760000000,"lon_nano":-122390000000},{"lat_nano":37760000000,"lon_nano":-122420000000}],"limit":100}'

Query Polygon with Group Filter

Filter polygon results by group ID.

curl -X POST http://localhost:3001/query/polygon -H "Content-Type: application/json" -d '{"vertices":[{"lat_nano":37790000000,"lon_nano":-122420000000},{"lat_nano":37790000000,"lon_nano":-122390000000},{"lat_nano":37760000000,"lon_nano":-122390000000},{"lat_nano":37760000000,"lon_nano":-122420000000}],"group_id":1,"limit":100}'

Query Polygon Error: Too Few Vertices

Polygon must have at least 3 vertices.

curl -X POST http://localhost:3001/query/polygon -H "Content-Type: application/json" -d '{"vertices":[{"lat_nano":37790000000,"lon_nano":-122420000000},{"lat_nano":37760000000,"lon_nano":-122420000000}],"limit":100}'

Expected response:

{"error":{"code":103,"message":"INVALID_POLYGON","details":"Polygon must have at least 3 vertices"}}

8. Query Latest

Get the most recent events across all entities.

curl -X POST http://localhost:3001/query/latest -H "Content-Type: application/json" -d '{"limit":100}'

Expected response:

{"events":[{"entity_id":"1005","lat_nano":37774900000,"lon_nano":-122419400000,"timestamp_ns":1706745700000000000}],"has_more":false,"cursor":null}

Query Latest with Group Filter

Filter latest events by group ID.

curl -X POST http://localhost:3001/query/latest -H "Content-Type: application/json" -d '{"limit":100,"group_id":1}'

Query Latest with Timestamp Filter

Only return events after a specific timestamp.

curl -X POST http://localhost:3001/query/latest -H "Content-Type: application/json" -d '{"limit":100,"since_ns":1706745600000000000}'

Server Operations

9. Ping

Health check endpoint.

curl http://localhost:3001/ping

Expected response (healthy):

{"pong":true}

10. Status

Get server status and statistics.

curl http://localhost:3001/status

Expected response:

{"events_count":1234,"entities_count":500,"index_bytes":56789012,"uptime_seconds":3600,"version":"1.0.0","cluster_state":"healthy","node_id":"node-1"}

11. Topology

Get cluster topology information.

curl http://localhost:3001/topology

Expected response (single node):

{"version":1,"num_shards":1,"replication_factor":1,"shards":[{"shard_id":0,"primary":{"node_id":"node-1","address":"127.0.0.1:3001","status":"healthy"},"replicas":[]}]}

TTL Operations

12. Set TTL

Set time-to-live for an entity (1 hour = 3600 seconds).

curl -X POST http://localhost:3001/ttl/set -H "Content-Type: application/json" -d '{"entity_id":"1001","ttl_seconds":3600}'

Expected response:

{"success":true,"expires_at_ns":1706749200000000000}

Set TTL - Entity Not Found

curl -X POST http://localhost:3001/ttl/set -H "Content-Type: application/json" -d '{"entity_id":"9999999","ttl_seconds":3600}'

Expected response:

{"error":{"code":3,"message":"ENTITY_NOT_FOUND","details":"Entity 9999999 does not exist"}}

13. Extend TTL

Add 1 hour to the existing TTL.

curl -X POST http://localhost:3001/ttl/extend -H "Content-Type: application/json" -d '{"entity_id":"1001","extend_by_seconds":3600}'

Expected response:

{"success":true,"previous_expires_at_ns":1706749200000000000,"new_expires_at_ns":1706752800000000000}

Extend TTL - No Existing TTL

curl -X POST http://localhost:3001/ttl/extend -H "Content-Type: application/json" -d '{"entity_id":"1002","extend_by_seconds":3600}'

Expected response:

{"error":{"code":106,"message":"NO_TTL_SET","details":"Entity 1002 has no TTL to extend"}}

14. Clear TTL

Remove TTL so entity never expires.

curl -X POST http://localhost:3001/ttl/clear -H "Content-Type: application/json" -d '{"entity_id":"1001"}'

Expected response:

{"success":true,"previous_expires_at_ns":1706749200000000000}

Clear TTL - No Existing TTL

Clearing TTL on an entity without TTL is not an error.

curl -X POST http://localhost:3001/ttl/clear -H "Content-Type: application/json" -d '{"entity_id":"1002"}'

Expected response:

{"success":true,"previous_expires_at_ns":null}

Pagination Example

For large result sets, use cursor-based pagination.

First Page

curl -X POST http://localhost:3001/query/radius -H "Content-Type: application/json" -d '{"center_lat_nano":37774900000,"center_lon_nano":-122419400000,"radius_mm":10000000,"limit":100}'

Response includes cursor if more results exist:

{"events":[...],"has_more":true,"cursor":"eyJsYXN0X2lkIjoxMDB9"}

Next Page

Use the cursor from previous response.

curl -X POST http://localhost:3001/query/radius -H "Content-Type: application/json" -d '{"center_lat_nano":37774900000,"center_lon_nano":-122419400000,"radius_mm":10000000,"limit":100,"cursor":"eyJsYXN0X2lkIjoxMDB9"}'

Continue Until Complete

Repeat until has_more is false:

{"events":[...],"has_more":false,"cursor":null}

Common Errors

Error Example Fix
Invalid latitude lat_nano > 90e9 Use range [-90e9, +90e9]
Invalid longitude lon_nano > 180e9 Use range [-180e9, +180e9]
Zero entity ID entity_id: "0" Use non-zero entity ID
Batch too large >10000 events Split into smaller batches
Entity not found TTL on missing entity Check entity exists
Invalid polygon < 3 vertices Provide at least 3 vertices

Coordinate Conversion Reference

Location Latitude Longitude lat_nano lon_nano
San Francisco 37.7749 -122.4194 37774900000 -122419400000
New York 40.7128 -74.0060 40712800000 -74006000000
London 51.5074 -0.1278 51507400000 -127800000
Tokyo 35.6762 139.6503 35676200000 139650300000
Sydney -33.8688 151.2093 -33868800000 151209300000
North Pole 90.0 0.0 90000000000 0
South Pole -90.0 0.0 -90000000000 0

Formula:

lat_nano = latitude_degrees * 1,000,000,000
lon_nano = longitude_degrees * 1,000,000,000
radius_mm = radius_meters * 1,000

See Also

Edit this page

ArcherDB Protocol Reference

ArcherDB uses HTTP/JSON for client communication. This document provides complete wire format details for custom client implementers.

Note: For most use cases, we recommend using one of the official SDKs rather than implementing the protocol directly.

Data Types

Coordinate Encoding

ArcherDB uses integer coordinates for precision and performance:

Type Unit Range Description
lat_nano nanodegrees (i64) -90,000,000,000 to +90,000,000,000 Latitude
lon_nano nanodegrees (i64) -180,000,000,000 to +180,000,000,000 Longitude
altitude_mm millimeters (i32) -10,000,000 to +100,000,000 Altitude (-10km to +100km)
velocity_mms mm/second (u32) 0 to 1,000,000,000 Speed (0 to 1000 m/s)
accuracy_mm millimeters (u32) 0 to 4,294,967,295 GPS accuracy radius
heading_cdeg centidegrees (u16) 0 to 35,999 Heading (0 = North, 9000 = East)
radius_mm millimeters (u64) 1 to 40,000,000,000 Query radius

Conversion formulas:

latitude_nano = latitude_degrees * 1,000,000,000
longitude_nano = longitude_degrees * 1,000,000,000
altitude_mm = altitude_meters * 1,000
velocity_mms = velocity_mps * 1,000
heading_cdeg = heading_degrees * 100
radius_mm = radius_meters * 1,000

Example: San Francisco (37.7749, -122.4194) becomes:

  • lat_nano: 37,774,900,000
  • lon_nano: -122,419,400,000

Precision: Nanodegrees provide ~0.1mm precision at the equator.

ID Encoding

Type Size JSON Representation Description
Entity ID u128 String (decimal or UUID) Unique identifier for tracked entity
Correlation ID u128 String Trip, session, or job correlation
User Data u128 String Application-specific metadata
Group ID u64 Number Fleet, region, or tenant identifier

Entity ID formats accepted:

  • Decimal string: "1001", "340282366920938463463374607431768211455"
  • UUID string: "550e8400-e29b-41d4-a716-446655440000"

Timestamp

Type Unit Description
timestamp_ns nanoseconds (u64) Unix epoch nanoseconds

Example: 1706745600000000000 = 2024-02-01 00:00:00 UTC

TTL (Time-to-Live)

Type Unit Range Description
ttl_seconds seconds (u32) 0 to 4,294,967,295 0 = never expire

Operations

ArcherDB supports 14 operations. Each section documents the HTTP method, endpoint, request format, and response format.

1. Insert Events (POST /events)

Insert new geo events. Fails if an event with the same entity_id already exists (use upsert for idempotent operations).

Request:

{
  "events": [
    {
      "entity_id": "1001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1,
      "ttl_seconds": 86400,
      "altitude_mm": 100000,
      "velocity_mms": 15000,
      "accuracy_mm": 5000,
      "heading_cdeg": 9000,
      "correlation_id": "11111",
      "user_data": "42",
      "flags": 0
    }
  ],
  "mode": "insert"
}

Request Fields:

Field Type Required Description
events array Yes Array of events (1 to 10,000)
events[].entity_id string Yes Unique entity identifier (non-zero)
events[].lat_nano i64 Yes Latitude in nanodegrees
events[].lon_nano i64 Yes Longitude in nanodegrees
events[].group_id u64 No Fleet/tenant identifier
events[].ttl_seconds u32 No Time-to-live (0 = never expire)
events[].altitude_mm i32 No Altitude in millimeters
events[].velocity_mms u32 No Speed in mm/second
events[].accuracy_mm u32 No GPS accuracy in mm
events[].heading_cdeg u16 No Heading in centidegrees
events[].correlation_id string No Correlation identifier
events[].user_data string No Application metadata
events[].flags u16 No Application-defined flags
mode string No "insert" (default) or "upsert"

Response (success):

{
  "results": [
    {"index": 0, "code": 0}
  ],
  "committed": true
}

Response (validation error):

{
  "results": [
    {"index": 0, "code": 9, "message": "LAT_OUT_OF_RANGE"}
  ],
  "committed": false
}

Response Fields:

Field Type Description
results array Per-event results (same order as request)
results[].index u32 Index in original batch
results[].code u16 Result code (0 = success)
results[].message string Error message (if code != 0)
committed bool True if batch committed

Common Error Codes:

Code Name Description
0 OK Success
7 ENTITY_ID_MUST_NOT_BE_ZERO Entity ID cannot be zero
9 LAT_OUT_OF_RANGE Latitude outside -90 to +90
10 LON_OUT_OF_RANGE Longitude outside -180 to +180

2. Upsert Events (POST /events with mode=upsert)

Insert or update geo events. If an event with the same entity_id exists, it is updated. Recommended for idempotent operations.

Request:

{
  "events": [
    {
      "entity_id": "1001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1
    }
  ],
  "mode": "upsert"
}

Response (created new):

{
  "results": [
    {"index": 0, "code": 0, "updated": false}
  ],
  "committed": true
}

Response (updated existing):

{
  "results": [
    {"index": 0, "code": 0, "updated": true}
  ],
  "committed": true
}

Additional Response Fields:

Field Type Description
results[].updated bool True if existing event was updated

3. Delete Entities (DELETE /entities)

Permanently delete all data for specified entities (supports GDPR erasure).

Request:

{
  "entity_ids": ["1001", "1002", "1003"]
}

Request Fields:

Field Type Required Description
entity_ids array Yes Entity IDs to delete (1 to 10,000)

Response (success):

{
  "deleted_count": 2,
  "not_found_count": 1
}

Response Fields:

Field Type Description
deleted_count u32 Number of entities deleted
not_found_count u32 Number of entities that didn’t exist

Response (empty request):

{
  "error": {
    "code": 101,
    "message": "EMPTY_REQUEST",
    "details": "entity_ids array is empty"
  }
}

4. Query by UUID (GET /entity/{id})

Get the most recent location for a single entity.

Request:

GET /entity/1001
GET /entity/550e8400-e29b-41d4-a716-446655440000

Response (found):

{
  "event": {
    "entity_id": "1001",
    "lat_nano": 37774900000,
    "lon_nano": -122419400000,
    "group_id": 1,
    "timestamp_ns": 1706745600000000000,
    "ttl_seconds": 86400,
    "altitude_mm": 100000,
    "velocity_mms": 15000,
    "accuracy_mm": 5000,
    "heading_cdeg": 9000
  },
  "found": true
}

Response (not found):

{
  "event": null,
  "found": false
}

Response Fields:

Field Type Description
event object GeoEvent data (null if not found)
found bool True if entity exists

5. Query UUID Batch (POST /entities/batch)

Get the most recent location for multiple entities in a single request.

Request:

{
  "entity_ids": ["1001", "1002", "1003"]
}

Request Fields:

Field Type Required Description
entity_ids array Yes Entity IDs to look up (1 to 10,000)

Response (partial match):

{
  "events": [
    {
      "entity_id": "1001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1,
      "timestamp_ns": 1706745600000000000
    },
    null,
    {
      "entity_id": "1003",
      "lat_nano": 40712800000,
      "lon_nano": -74006000000,
      "group_id": 2,
      "timestamp_ns": 1706745700000000000
    }
  ]
}

Response Fields:

Field Type Description
events array Events in same order as request (null for not found)

Response (batch too large):

{
  "error": {
    "code": 300,
    "message": "BATCH_TOO_LARGE",
    "details": "Maximum batch size is 10000"
  }
}

6. Query Radius (POST /query/radius)

Find all entities within a radius of a center point.

Request:

{
  "center_lat_nano": 37774900000,
  "center_lon_nano": -122419400000,
  "radius_mm": 1000000,
  "limit": 100,
  "group_id": 1,
  "cursor": null
}

Request Fields:

Field Type Required Description
center_lat_nano i64 Yes Center latitude in nanodegrees
center_lon_nano i64 Yes Center longitude in nanodegrees
radius_mm u64 Yes Radius in millimeters (1 to 40,000,000,000)
limit u32 No Maximum results per page (default: 1000, max: 10000)
group_id u64 No Filter by group ID
cursor string No Pagination cursor from previous response

Note: radius_mm is radius in millimeters. 1,000,000 mm = 1 km.

Response (with results):

{
  "events": [
    {
      "entity_id": "1001",
      "lat_nano": 37774500000,
      "lon_nano": -122419000000,
      "distance_mm": 50000,
      "group_id": 1,
      "timestamp_ns": 1706745600000000000
    },
    {
      "entity_id": "1002",
      "lat_nano": 37775000000,
      "lon_nano": -122418000000,
      "distance_mm": 150000,
      "group_id": 1,
      "timestamp_ns": 1706745650000000000
    }
  ],
  "has_more": true,
  "cursor": "eyJsYXN0X2lkIjogMTAwMn0="
}

Response Fields:

Field Type Description
events array Matching events
events[].distance_mm u64 Distance from center in millimeters
has_more bool True if more results available
cursor string Cursor for next page (present if has_more)

Response (empty):

{
  "events": [],
  "has_more": false,
  "cursor": null
}

Response (invalid radius):

{
  "error": {
    "code": 101,
    "message": "INVALID_RADIUS",
    "details": "radius_mm must be positive"
  }
}

7. Query Polygon (POST /query/polygon)

Find all entities within a polygon boundary.

Request:

{
  "vertices": [
    {"lat_nano": 37790000000, "lon_nano": -122420000000},
    {"lat_nano": 37790000000, "lon_nano": -122390000000},
    {"lat_nano": 37760000000, "lon_nano": -122390000000},
    {"lat_nano": 37760000000, "lon_nano": -122420000000}
  ],
  "limit": 100,
  "group_id": null,
  "cursor": null
}

Request Fields:

Field Type Required Description
vertices array Yes Polygon vertices (3 to 1000 points)
vertices[].lat_nano i64 Yes Vertex latitude in nanodegrees
vertices[].lon_nano i64 Yes Vertex longitude in nanodegrees
limit u32 No Maximum results per page (default: 1000, max: 10000)
group_id u64 No Filter by group ID
cursor string No Pagination cursor from previous response

Winding Order:

  • Outer boundary: Counter-clockwise
  • Polygon is auto-closed if first and last vertices differ

Response (with results):

{
  "events": [
    {
      "entity_id": "1001",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "group_id": 1,
      "timestamp_ns": 1706745600000000000
    }
  ],
  "has_more": false,
  "cursor": null
}

Response (invalid polygon):

{
  "error": {
    "code": 103,
    "message": "INVALID_POLYGON",
    "details": "Polygon must have at least 3 vertices"
  }
}

8. Query Latest (POST /query/latest)

Get the most recent events across all entities.

Request:

{
  "limit": 100,
  "group_id": 1,
  "since_ns": 1706745600000000000,
  "cursor": null
}

Request Fields:

Field Type Required Description
limit u32 No Maximum results per page (default: 1000, max: 10000)
group_id u64 No Filter by group ID
since_ns u64 No Only return events after this timestamp
cursor string No Pagination cursor from previous response

Response (with results):

{
  "events": [
    {
      "entity_id": "1005",
      "lat_nano": 37774900000,
      "lon_nano": -122419400000,
      "timestamp_ns": 1706745700000000000,
      "group_id": 1
    },
    {
      "entity_id": "1004",
      "lat_nano": 40712800000,
      "lon_nano": -74006000000,
      "timestamp_ns": 1706745650000000000,
      "group_id": 1
    }
  ],
  "has_more": true,
  "cursor": "eyJ0cyI6IDE3MDY3NDU2NTAwMDAwMDAwMDB9"
}

Response Fields:

Field Type Description
events array Events ordered by timestamp (most recent first)
has_more bool True if more results available
cursor string Cursor for next page (present if has_more)

Response (empty):

{
  "events": [],
  "has_more": false,
  "cursor": null
}

9. Ping (GET /ping)

Health check endpoint. Returns server availability status.

Request:

GET /ping

Response (healthy):

{
  "pong": true
}

Response (unhealthy):

HTTP 503 Service Unavailable

{
  "pong": false,
  "reason": "cluster not ready"
}

Response Fields:

Field Type Description
pong bool True if server is healthy
reason string Reason for unhealthy status (only if pong=false)

10. Status (GET /status)

Get server status and statistics.

Request:

GET /status

Response:

{
  "events_count": 125000,
  "entities_count": 42000,
  "index_bytes": 56789012,
  "uptime_seconds": 86400,
  "version": "1.0.0",
  "cluster_state": "healthy",
  "node_id": "node-1"
}

Response Fields:

Field Type Description
events_count u64 Total number of events stored
entities_count u64 Number of unique entities
index_bytes u64 Size of geospatial index in bytes
uptime_seconds u64 Server uptime in seconds
version string ArcherDB version
cluster_state string Cluster health status
node_id string Identifier of responding node

11. Get Topology (GET /topology)

Get cluster topology including shard and replica information.

Request:

GET /topology

Response (single node):

{
  "version": 1,
  "num_shards": 1,
  "replication_factor": 1,
  "shards": [
    {
      "shard_id": 0,
      "primary": {
        "node_id": "node-1",
        "address": "127.0.0.1:3001",
        "status": "healthy"
      },
      "replicas": []
    }
  ]
}

Response (clustered):

{
  "version": 3,
  "num_shards": 4,
  "replication_factor": 3,
  "shards": [
    {
      "shard_id": 0,
      "primary": {
        "node_id": "node-1",
        "address": "10.0.0.1:3001",
        "status": "healthy"
      },
      "replicas": [
        {
          "node_id": "node-2",
          "address": "10.0.0.2:3001",
          "status": "healthy"
        },
        {
          "node_id": "node-3",
          "address": "10.0.0.3:3001",
          "status": "healthy"
        }
      ]
    }
  ]
}

Response Fields:

Field Type Description
version u64 Topology version (increments on changes)
num_shards u32 Number of shards in cluster
replication_factor u32 Number of replicas per shard
shards array Shard information
shards[].shard_id u32 Shard identifier
shards[].primary object Primary node for this shard
shards[].primary.node_id string Node identifier
shards[].primary.address string Node address (host:port)
shards[].primary.status string Node health status
shards[].replicas array Replica nodes for this shard

12. Set TTL (POST /ttl/set)

Set or replace the time-to-live for an entity.

Request:

{
  "entity_id": "1001",
  "ttl_seconds": 86400
}

Request Fields:

Field Type Required Description
entity_id string Yes Entity to set TTL on
ttl_seconds u32 Yes TTL in seconds (0 = never expire)

Response (success):

{
  "success": true,
  "expires_at_ns": 1706832000000000000
}

Response Fields:

Field Type Description
success bool True if TTL was set
expires_at_ns u64 Expiration timestamp in nanoseconds

Response (entity not found):

{
  "error": {
    "code": 3,
    "message": "ENTITY_NOT_FOUND",
    "details": "Entity 1001 does not exist"
  }
}

13. Extend TTL (POST /ttl/extend)

Extend the time-to-live for an entity by a specified duration.

Request:

{
  "entity_id": "1001",
  "extend_by_seconds": 3600
}

Request Fields:

Field Type Required Description
entity_id string Yes Entity to extend TTL on
extend_by_seconds u32 Yes Seconds to add to current TTL

Response (success):

{
  "success": true,
  "previous_expires_at_ns": 1706832000000000000,
  "new_expires_at_ns": 1706835600000000000
}

Response Fields:

Field Type Description
success bool True if TTL was extended
previous_expires_at_ns u64 Previous expiration timestamp
new_expires_at_ns u64 New expiration timestamp

Response (no existing TTL):

{
  "error": {
    "code": 106,
    "message": "NO_TTL_SET",
    "details": "Entity 1001 has no TTL to extend"
  }
}

14. Clear TTL (POST /ttl/clear)

Remove the time-to-live for an entity (entity will never expire).

Request:

{
  "entity_id": "1001"
}

Request Fields:

Field Type Required Description
entity_id string Yes Entity to clear TTL on

Response (success):

{
  "success": true,
  "previous_expires_at_ns": 1706832000000000000
}

Response (no existing TTL):

{
  "success": true,
  "previous_expires_at_ns": null
}

Response Fields:

Field Type Description
success bool Always true if entity exists
previous_expires_at_ns u64 Previous expiration (null if no TTL was set)

Error Handling

HTTP Status Codes

Status Description
200 Success
400 Validation error (bad request format, invalid coordinates)
404 Not found (entity lookup only)
500 Internal error
503 Cluster unavailable

Error Response Format

All errors return a JSON object with error details:

{
  "error": {
    "code": 100,
    "message": "INVALID_COORDINATES",
    "details": "latitude 100.0 out of range [-90, +90]"
  }
}
Field Type Description
error.code u16 Numeric error code
error.message string Error name
error.details string Human-readable description

Error Code Ranges

Range Category General Handling
0 Success Operation completed
1-99 Protocol Check client version, message format
100-199 Validation Fix request parameters
200-299 State Check cluster health, retry if transient
300-399 Resource Reduce batch size, check limits
400-499 Security Check external gateway/service authn/authz policy
500-599 Internal Open an issue with logs and reproduction details

Retryable vs Non-Retryable Errors

Retryable errors (safe to retry with backoff):

  • 211 - Cluster unavailable (no quorum)
  • 220 - Not shard leader
  • 222 - Resharding in progress
  • Network timeouts

Non-retryable errors (fix request first):

  • 7 - Entity ID must not be zero
  • 9 - Latitude out of range
  • 10 - Longitude out of range
  • 100-199 - Validation errors
  • 300 - Batch too large

For complete error reference, see Error Codes.


Pagination

All query operations (radius, polygon, latest) use cursor-based pagination.

Request Parameters

Field Type Description
limit u32 Maximum events per page (default: 1000, max: 10000)
cursor string Opaque pagination token from previous response

Response Fields

Field Type Description
has_more bool True if more results exist
cursor string Token for next page (present if has_more)

Pagination Flow

1. First request:

{
  "center_lat_nano": 37774900000,
  "center_lon_nano": -122419400000,
  "radius_mm": 10000000,
  "limit": 100
}

2. Response with cursor:

{
  "events": [...],
  "has_more": true,
  "cursor": "abc123..."
}

3. Next page request:

{
  "center_lat_nano": 37774900000,
  "center_lon_nano": -122419400000,
  "radius_mm": 10000000,
  "limit": 100,
  "cursor": "abc123..."
}

4. Continue until:

{
  "events": [...],
  "has_more": false,
  "cursor": null
}

Best Practices

  • Use limit of 1000 for most cases (good balance of latency vs round trips)
  • Treat cursors as opaque - do not parse or modify them
  • Cursors may expire if underlying data changes significantly
  • Results are returned in deterministic order (S2 cell ID based)

Authentication

Authentication is enforced outside ArcherDB in the API/service boundary.

ArcherDB protocol endpoints should only be reachable from trusted internal networks. Use gateway/service-mesh policy for:

  • Client identity
  • Authentication
  • Authorization

Content Types

Request

All POST/DELETE requests must include:

Content-Type: application/json

Response

All responses return:

Content-Type: application/json

See Also

Edit this page

ArcherDB Architecture

This document provides a comprehensive deep-dive into ArcherDB’s architecture, explaining how the system works internally and why specific design decisions were made.

Key Concepts

Before diving into details, here are the essential concepts that define ArcherDB’s behavior:

Concept What It Means for Users
Linearizability All operations appear to execute atomically in a single global order. A read always returns the result of the most recent write - no stale data, no anomalies.
Quorum A majority of replicas (2 of 3, or 3 of 5) must agree before any write is committed. This guarantees durability even if minority replicas fail.
Leader Election When the primary fails, remaining replicas automatically elect a new leader within seconds. No manual intervention required.
S2 Cells Locations are indexed using Google’s S2 geometry library. Nearby points have numerically close cell IDs, enabling efficient spatial queries.
LSM Tree Write-optimized storage that achieves high throughput by writing sequentially. Background compaction keeps read performance consistent.

Quick Links:

Table of Contents

  1. Introduction
  2. System Overview
  3. Viewstamped Replication (VSR)
  4. LSM-Tree Storage
  5. S2 Geospatial Indexing
  6. RAM Index
  7. Sharding
  8. Replication
  9. Summary

Introduction

ArcherDB is a distributed geospatial database designed for real-time location tracking at scale. It provides sub-millisecond queries for millions of moving entities while guaranteeing strong consistency and durability.

Design Principles

ArcherDB follows three core principles:

  1. Correctness First: The system never returns stale data or loses acknowledged writes. Consensus (VSR) ensures all replicas agree on operation order, and durability guarantees survive any single point of failure.

  2. No Compromises: Rather than degrading gracefully under resource pressure, ArcherDB demands adequate resources and exposes problems through metrics and traces. This philosophy prevents silent data corruption and makes capacity issues visible before they become critical.

  3. Purpose-Built for Geospatial: Every component is optimized for location data - from the S2 spatial indexing to the composite key design that enables efficient range queries over space and time.

Target Use Cases

ArcherDB excels at workloads where you need to:

  • Fleet Management: Track thousands of vehicles in real-time, query “which trucks are within 5km of this warehouse?”
  • Asset Tracking: Monitor equipment, containers, or inventory across facilities with instant location lookup
  • Ride-Sharing & Delivery: Match riders to nearby drivers, optimize delivery routes based on current positions
  • Logistics & Supply Chain: Real-time visibility into shipment locations with historical trajectory analysis

What Makes ArcherDB Different

Unlike general-purpose databases with geospatial extensions (PostGIS, MongoDB) or in-memory stores (Redis/Tile38), ArcherDB is built from the ground up for distributed location tracking:

Capability ArcherDB PostGIS Redis/Valkey/Tile38
Linearizable consistency Yes (VSR) Yes (single node) No
Automatic failover Yes No Cluster mode
Purpose-built spatial index S2 (Hilbert curve) GiST/R-tree Geohash
Write durability fsync + consensus before reply fsync before reply None by default
Measured insert throughput¹ 831K events/s (durable) 31K rows/s 174–202K ops/s (volatile); 109K (fsync)
Multi-region replication Yes (async) Manual No

¹ Single node, identical hardware, August 2026 — methodology, query-latency results, and reproduction steps in benchmarks/single-node-2026-08.md. ArcherDB’s number is fully durable (O_DIRECT+O_DSYNC WAL, consensus-committed); the Valkey GEOADD ceiling is with persistence disabled entirely.


System Overview

ArcherDB consists of several interconnected components that work together to provide fast, consistent geospatial operations.

High-Level Architecture

flowchart TB
    subgraph Clients["Client Applications"]
        SDK[SDK<br/>Python/Go/Java/Node/C]
    end

    subgraph Cluster["ArcherDB Cluster"]
        subgraph Replica0["Replica 0 (Primary)"]
            VSR0[VSR Consensus]
            SM0[State Machine]
            LSM0[LSM-Tree]
            S2IDX0[S2 Index]
            RAM0[RAM Index]
        end

        subgraph Replica1["Replica 1 (Backup)"]
            VSR1[VSR Consensus]
            SM1[State Machine]
        end

        subgraph Replica2["Replica 2 (Backup)"]
            VSR2[VSR Consensus]
            SM2[State Machine]
        end

        VSR0 <-->|Prepare/Commit| VSR1
        VSR1 <-->|Prepare/Commit| VSR2
        VSR2 <-->|Prepare/Commit| VSR0
    end

    subgraph CrossRegion["Cross-Region Replication"]
        S3[(S3 Bucket)]
        Follower[Follower Region]
    end

    SDK -->|Request| VSR0
    VSR0 -->|Reply| SDK
    SM0 --> LSM0
    SM0 --> S2IDX0
    SM0 --> RAM0
    LSM0 -->|WAL Shipping| S3
    S3 -->|Apply| Follower

Component Responsibilities

Component Responsibility Key Feature
VSR Consensus Ensures all replicas agree on operation order Linearizability, automatic failover
State Machine Executes operations deterministically Geospatial operations, TTL expiration
LSM-Tree Durable sorted storage for GeoEvents Write optimization, range scans
S2 Index Spatial queries (radius, polygon) Hilbert curve locality
RAM Index O(1) latest position lookup 64-byte cache-aligned entries

Request Flow

When a client sends a request (e.g., “insert location for vehicle X”):

  1. SDK serializes the request and sends to the primary replica
  2. VSR assigns a timestamp and broadcasts to all replicas (Prepare phase)
  3. Backups acknowledge receipt (Prepare OK)
  4. Primary commits after quorum acknowledgment
  5. State Machine executes the operation deterministically
  6. LSM-Tree persists the GeoEvent durably
  7. RAM Index updates the latest position cache
  8. S2 Index enables future spatial queries
  9. Client receives success response

Viewstamped Replication (VSR)

VSR is ArcherDB’s consensus protocol, providing strong consistency guarantees across replicas. It ensures that even if replicas fail, the system continues operating correctly without data loss.

What VSR Provides

  • Linearizability: All operations appear to execute atomically in a single global order. A read always sees the result of all previously committed writes.
  • Durability: Once a write is acknowledged, it survives any minority of replica failures (e.g., 1 failure in a 3-node cluster, 2 failures in a 5-node cluster).
  • Automatic Failover: When the primary fails, backups elect a new primary within seconds without manual intervention.

Why VSR Over Raft or Paxos?

ArcherDB inherits VSR from TigerBeetle, which chose it for several reasons:

  1. No Log Truncation: Unlike Raft, VSR never truncates committed entries. This simplifies crash recovery and eliminates a class of subtle bugs.

  2. Deterministic Replay: The same sequence of operations produces identical state on all replicas, enabling VOPR (Viewstamped Operation Prover) simulation testing.

  3. Battle-Tested: TigerBeetle has run VSR through millions of hours of deterministic simulation, finding and fixing edge cases that would be nearly impossible to discover through traditional testing.

How VSR Works

VSR operates in two main phases: Prepare (replication) and Commit (execution).

sequenceDiagram
    participant C as Client
    participant P as Primary
    participant B1 as Backup 1
    participant B2 as Backup 2

    C->>P: Request (insert GeoEvent)
    P->>P: Assign timestamp (op=42)

    par Broadcast to Backups
        P->>B1: Prepare(op=42, checksum)
        P->>B2: Prepare(op=42, checksum)
    end

    B1->>P: Prepare OK(op=42)
    B2->>P: Prepare OK(op=42)

    Note over P: Quorum reached (2/3)
    P->>P: Commit & Execute

    P->>C: Reply (success)

    par Notify Backups
        P->>B1: Commit(op=42)
        P->>B2: Commit(op=42)
    end

    B1->>B1: Execute
    B2->>B2: Execute

Key Concepts

View: A configuration where one replica is designated as primary. The view number increases monotonically when leadership changes. In view 0, replica 0 is primary; in view 1, replica 1 is primary; and so on (modulo replica count).

Prepare: The primary assigns a monotonically increasing operation number and broadcasts the operation to all backups. The operation is hash-chained to its predecessor for integrity verification.

Commit: After receiving acknowledgments from a quorum (majority) of replicas, the primary commits the operation. This guarantees the operation is durable - even if the primary fails immediately after, the operation can be recovered.

View Change: When backups detect the primary has failed (missed heartbeats), they initiate a view change to elect a new primary. The protocol ensures no committed operations are lost during the transition.

View Change Protocol

When the primary fails, VSR performs a three-phase view change:

sequenceDiagram
    participant B1 as Backup 1
    participant B2 as Backup 2
    participant NP as New Primary

    Note over B1,B2: Primary missed heartbeats

    B1->>B2: START_VIEW_CHANGE(view=1)
    B2->>B1: START_VIEW_CHANGE(view=1)

    Note over NP: Quorum for view change

    B1->>NP: DO_VIEW_CHANGE(log state)
    B2->>NP: DO_VIEW_CHANGE(log state)

    Note over NP: Select longest log

    NP->>B1: START_VIEW(view=1, log)
    NP->>B2: START_VIEW(view=1, log)

    Note over B1,NP: Normal operation resumes

The new primary collects the log state from a quorum and selects the most complete version, ensuring no committed operations are lost.

Key Invariants

VSR maintains several invariants that guarantee correctness:

  1. Op Ordering: Operation numbers strictly increase, and the hash chain enforces that all replicas process operations in the same order.

  2. Commit Safety: An operation is only committed after quorum acknowledgment, preventing data loss on primary failure.

  3. View Monotonicity: View numbers only increase, preventing split-brain scenarios where two replicas both believe they are primary.

  4. Primary Uniqueness: At most one primary exists per view, determined by primary_index = view % replica_count.

For deeper technical details on VSR internals, see vsr_understanding.md.


LSM-Tree Storage

ArcherDB uses a Log-Structured Merge-tree (LSM) for persistent storage, optimized for write-heavy location tracking workloads.

What LSM Provides

  • Write Optimization: Sequential writes to disk (append-only) achieve much higher throughput than random writes (B-tree updates).
  • Sorted Storage: Data is sorted by key, enabling efficient range scans - crucial for “all events in this time range” or “all events in this spatial cell” queries.
  • Space Reclamation: Background compaction merges levels and eliminates deleted/expired data without blocking writes.

Why LSM Over B-Tree?

Location tracking is inherently write-heavy: every vehicle reports its position every few seconds. LSM trees excel at this workload:

Metric LSM-Tree B-Tree
Write pattern Sequential (fast) Random (slow)
Write amplification 10-30x 2-3x
Read amplification Higher (check multiple levels) Lower (single tree)
Space amplification Lower (compaction) Higher (page splits)

For ArcherDB’s workload (many writes, fewer reads, mostly recent data), LSM’s trade-offs are favorable.

LSM Structure

Data flows through the LSM tree in levels:

flowchart TB
    subgraph Memory
        MT[Memtable<br/>Recent Writes]
    end

    subgraph Level0["Level 0 (Unsorted)"]
        L0A[SSTable]
        L0B[SSTable]
        L0C[SSTable]
    end

    subgraph Level1["Level 1 (Sorted)"]
        L1[SSTable Range A-F]
        L1B[SSTable Range G-M]
        L1C[SSTable Range N-Z]
    end

    subgraph Level2["Level 2 (Sorted, 10x larger)"]
        L2[SSTables...]
    end

    subgraph LevelN["Level N (Sorted, 10^N larger)"]
        LN[SSTables...]
    end

    MT -->|Flush| L0A
    L0A -->|Compact| L1
    L0B -->|Compact| L1
    L0C -->|Compact| L1
    L1 -->|Compact| L2
    L2 -->|Compact| LN

Write Path

  1. Memtable: Writes first go to an in-memory sorted structure (memtable)
  2. Flush: When the memtable fills, it’s flushed to Level 0 as an immutable SSTable
  3. Level 0: Contains recent SSTables with potentially overlapping key ranges
  4. Compaction: Background process merges L0 files into L1, then L1 into L2, etc.

Each level is ~10x larger than the previous (configurable via lsm_growth_factor).

Read Path

To read a key:

  1. Check Memtable: If key is in memory, return immediately (fastest)
  2. Check Level 0: Scan all L0 files (they may overlap)
  3. Binary Search Levels 1-N: Each level is sorted, so binary search finds the right file, then the right block within the file

ArcherDB uses key-range filtering instead of bloom filters: each file’s index block stores min/max keys, allowing quick elimination of files that can’t contain the target key.

Compaction

Compaction keeps the LSM tree healthy by:

  • Merging overlapping key ranges: Combines files with the same keys, keeping only the newest version
  • Eliminating tombstones: Deleted data is physically removed when tombstones reach the deepest level
  • Maintaining sorted order: Each level (except L0) has non-overlapping key ranges

ArcherDB’s compaction has dedicated I/O resources (18 read IOPS, 17 write IOPS) that are separate from foreground operations, preventing compaction from causing latency spikes.

Tuning

Key tuning parameters:

Runtime parameter Shared value across tiers Effect
lsm_levels 8 Higher capacity envelope per tree
lsm_growth_factor 8 Balanced write/read amplification
lsm_compaction_ops 128 Larger memtable, fewer flushes
block_size 1 MiB Higher sequential throughput
message_size_max 10 MiB Large request envelope; avoid transport bottlenecks

Capacity differences are enforced only by quotas:

Tier RAM index default Storage default / max
lite 128 MiB 16 GiB / 16 GiB
standard 16 GiB 256 GiB / 1 TiB
pro 32 GiB 2 TiB / 8 TiB
enterprise 64 GiB 16 TiB / 64 TiB
ultra 128 GiB 64 TiB / 256 TiB

For detailed tuning guidance, see lsm-tuning.md.


S2 Geospatial Indexing

S2 is Google’s spherical geometry library that ArcherDB uses for spatial indexing. It provides efficient “find all entities within this area” queries.

What S2 Is

S2 projects Earth’s surface onto a cube, then unfolds the cube into a 2D plane using a Hilbert curve. This creates a hierarchical decomposition where:

  • Level 0: 6 cells (cube faces), each ~85 million km^2
  • Level 30: ~4.6 billion cells per face, each ~0.74 cm^2

Each cell has a 64-bit ID that encodes both its position and level.

Why S2 Over Geohash or R-Tree?

Geohash (used by Redis) has significant problems:

  • Edge discontinuities: Adjacent cells at the equator or prime meridian may have very different hash values
  • No hierarchy: Parent/child relationships require string manipulation
  • Polar distortion: Cells become extremely thin near poles

R-Trees (used by PostGIS) have different trade-offs:

  • Dynamic rebalancing: Tree structure changes with inserts, complicating distributed systems
  • Non-deterministic: Different insert orders produce different tree shapes
  • Memory overhead: Internal node structures consume significant memory

S2’s advantages:

  • Locality preservation: The Hilbert curve ensures nearby points have numerically close cell IDs, making range scans efficient
  • Deterministic: Same coordinates always produce the same cell ID
  • Hierarchical: Parent and child cells are computed with bit operations (O(1))
  • No edge discontinuities: The cube projection handles Earth’s curvature gracefully

How S2 Cells Work

The S2 cell hierarchy forms a quad-tree where each cell has exactly 4 children:

flowchart TB
    subgraph Level0["Level 0 (Face 0)"]
        F0[Cell 0x...]
    end

    subgraph Level1["Level 1"]
        C1A[Cell 0x...0]
        C1B[Cell 0x...1]
        C1C[Cell 0x...2]
        C1D[Cell 0x...3]
    end

    subgraph Level2["Level 2"]
        C2A[4 children]
        C2B[4 children]
        C2C[4 children]
        C2D[4 children]
    end

    subgraph Level30["Level 30 (Leaf)"]
        L30[~0.74 cm^2]
    end

    F0 --> C1A
    F0 --> C1B
    F0 --> C1C
    F0 --> C1D
    C1A --> C2A
    C1B --> C2B
    C1C --> C2C
    C1D --> C2D
    C2A -.->|28 more levels| L30

Cell IDs are structured so that a cell’s children can be computed with simple bit operations:

Parent cell:  0x89c258...00  (level 15)
Child 0:      0x89c258...00  (level 16)
Child 1:      0x89c258...40  (level 16)
Child 2:      0x89c258...80  (level 16)
Child 3:      0x89c258...c0  (level 16)

Query Flow

Radius Query: “Find all entities within 1km of this point”

  1. Compute covering: Generate S2 cells that cover the 1km circle
  2. Scan cells: For each covering cell, query the LSM tree for events in that cell range
  3. Filter by distance: For each candidate, compute exact distance and filter out false positives
flowchart LR
    subgraph Input
        P[Center Point<br/>+ Radius]
    end

    subgraph S2["S2 Covering"]
        C1[Cell A]
        C2[Cell B]
        C3[Cell C]
    end

    subgraph LSM["LSM Scans"]
        R1[Events in A]
        R2[Events in B]
        R3[Events in C]
    end

    subgraph Filter["Distance Filter"]
        F[Haversine<br/>distance check]
    end

    subgraph Output
        O[Matching<br/>Events]
    end

    P --> C1
    P --> C2
    P --> C3
    C1 --> R1
    C2 --> R2
    C3 --> R3
    R1 --> F
    R2 --> F
    R3 --> F
    F --> O

Polygon Query: “Find all entities within this delivery zone”

  1. Compute covering: Generate S2 cells that cover the polygon
  2. Scan cells: Query LSM for events in covering cells
  3. Point-in-polygon test: For candidates near polygon edges, verify with ray-casting algorithm

Performance Characteristics

Query complexity: O(n) where n = entities in covering cells

The covering algorithm aims for ~8 cells by default (configurable). With good cell selection:

  • A 1km radius query in a city might scan 10,000 candidates to return 100 results
  • A polygon covering a neighborhood might scan 50,000 candidates to return 500 results

The key insight is that S2’s Hilbert curve ordering means these candidates are stored contiguously in the LSM tree, enabling efficient sequential reads.


RAM Index

The RAM Index provides O(1) lookup for “where is entity X right now?” queries - the most common operation in fleet tracking.

What RAM Index Provides

  • O(1) Lookup: Hash table lookup for any entity’s latest position
  • Cache-Line Aligned: 64-byte entries fit exactly in CPU cache lines for optimal performance
  • Lock-Free Reads: Atomic operations enable concurrent reads without blocking

Why a Separate RAM Index?

Querying the LSM tree for a single entity requires:

  1. Checking the memtable (fast)
  2. Potentially checking multiple L0 files (slower)
  3. Binary searching through levels (slowest)

For “where is vehicle X?” queries that happen thousands of times per second, this overhead is unacceptable. The RAM index provides direct access:

flowchart LR
    subgraph Query["Query: entity_id=X"]
        Q[entity_id]
    end

    subgraph RAM["RAM Index"]
        HT[Hash Table<br/>O(1) lookup]
    end

    subgraph Entry["Index Entry (64 bytes)"]
        E[entity_id: 16B<br/>composite_id: 16B<br/>lat_nano: 8B<br/>lon_nano: 8B<br/>timestamp: 8B<br/>...]
    end

    Q --> HT
    HT --> E

Design Details

Index Entry Structure (64 bytes, cache-line aligned):

Field Size Purpose
entity_id 16 bytes Hash table key
composite_id 16 bytes S2 cell + timestamp for LSM lookup
lat_nano 8 bytes Latest latitude (nanodegrees)
lon_nano 8 bytes Latest longitude (nanodegrees)
timestamp 8 bytes When this position was recorded
flags 2 bytes Status flags (moving, offline, etc.)
reserved 6 bytes Future use

Memory Formula:

The raw index entry is 64 bytes. Runtime sizing uses 96 bytes per hash-table slot because the state machine also reserves spatial scan-helper arrays per slot.

RAM index budget = entity_count / load_factor * 96 bytes

For 1 billion entities at 0.70 load factor:
RAM = 1B / 0.70 * 96 = ~137 GB

This is significant but predictable. ArcherDB’s “no compromises” philosophy means you provision adequate memory rather than accepting degraded performance.

Concurrency Model

  • Writes: Single-threaded during VSR commit phase (guaranteed by consensus)
  • Reads: Lock-free atomic loads (multiple concurrent readers)
  • Updates: Last-Write-Wins (LWW) semantics - newer timestamp always wins

Persistence

The RAM index supports two modes:

  1. Heap Mode: Faster, but lost on restart. Rebuilt by scanning the LSM tree.
  2. Mmap Mode: File-backed with MAP_SHARED. Survives restarts, but slightly slower.

For most deployments, heap mode with fast LSM recovery is preferred.


Sharding

Sharding distributes data across multiple ArcherDB clusters to scale beyond a single node’s capacity.

Why Shard?

A single ArcherDB cluster (3-5 nodes) handles approximately:

  • 1 million writes/second
  • 100 billion events storage
  • 100 million entities in RAM index

For larger deployments, sharding provides:

  • Horizontal Scale: Add more shards to handle more entities
  • Geographic Distribution: Place shards closer to data sources
  • Isolation: A problem in one shard doesn’t affect others

Sharding Strategy: Jump Hash

ArcherDB uses Jump Consistent Hash for shard assignment:

shard = jump_hash(entity_id, num_shards)

Why Jump Hash?

Algorithm Resharding Movement Memory Uniformity
Modulo ~100% (power-of-2 only) O(1) Good
Consistent Hash Ring ~1/n O(n) Requires virtual nodes
Jump Hash ~1/n O(1) Excellent

Jump Hash achieves optimal resharding (only 1/n entities move when adding a shard) with no memory overhead - the algorithm is a pure function of the key and shard count.

Shard Routing

Clients compute the shard locally - no coordinator needed for single-entity operations:

flowchart TB
    subgraph Client
        SDK[SDK]
        JH[jump_hash<br/>entity_id, N]
    end

    subgraph Shards
        S0[Shard 0<br/>Cluster A]
        S1[Shard 1<br/>Cluster B]
        S2[Shard 2<br/>Cluster C]
    end

    SDK --> JH
    JH -->|shard=0| S0
    JH -->|shard=1| S1
    JH -->|shard=2| S2

Cross-Shard Queries

Radius and polygon queries may span multiple shards. The coordinator pattern handles this:

  1. Coordinator receives query
  2. Fan out to all relevant shards in parallel
  3. Aggregate results
  4. Return combined result set
flowchart TB
    subgraph Client
        C[Query: radius 5km]
    end

    subgraph Coordinator
        CO[Coordinator]
    end

    subgraph Shards
        S0[Shard 0]
        S1[Shard 1]
        S2[Shard 2]
    end

    subgraph Aggregator
        A[Merge & Sort]
    end

    C --> CO
    CO -->|Query| S0
    CO -->|Query| S1
    CO -->|Query| S2
    S0 -->|Results| A
    S1 -->|Results| A
    S2 -->|Results| A
    A --> C

For optimal performance, entities that are frequently queried together (same fleet, same region) should hash to the same shard. The group_id field enables this.


Replication

ArcherDB supports two replication modes for different consistency requirements.

Synchronous Replication (Within Region)

Within a single region, VSR provides synchronous replication:

  • Strong Consistency: Reads always see the latest committed write
  • Automatic Failover: If primary fails, backup takes over in seconds
  • Quorum Writes: Writes acknowledged after majority of replicas confirm

This is the default mode for a single ArcherDB cluster.

Asynchronous Replication (Cross-Region)

For multi-region deployments, ArcherDB uses asynchronous log shipping:

flowchart TB
    subgraph Primary["Primary Region (US-East)"]
        P0[Replica 0]
        P1[Replica 1]
        P2[Replica 2]
        P0 <-->|VSR Sync| P1
        P1 <-->|VSR Sync| P2
    end

    subgraph S3["S3 Bucket"]
        WAL[(WAL Segments)]
    end

    subgraph Spillover["Disk Spillover"]
        SP[(Local Disk)]
    end

    subgraph Follower["Follower Region (EU-West)"]
        F0[Replica 0]
        F1[Replica 1]
        F2[Replica 2]
        F0 <-->|VSR Sync| F1
        F1 <-->|VSR Sync| F2
    end

    P0 -->|Ship WAL| WAL
    P0 -.->|Fallback| SP
    SP -.->|Retry| WAL
    WAL -->|Pull WAL| F0

Cross-Region Flow

  1. Primary commits via VSR (synchronous within region)
  2. WAL entries shipped to S3 bucket (asynchronous)
  3. Follower pulls from S3 and applies entries
  4. Eventual consistency: Followers lag primary by seconds to minutes

Consistency Model

Scope Consistency Lag
Within region Strong (linearizable) 0
Cross-region Eventual Seconds to minutes

Applications can choose:

  • Read from primary: Always see latest data, but higher latency from distant clients
  • Read from follower: Lower latency, but may see stale data

Failure Handling

S3 Unavailable: Entries spill to local disk, then retry S3 when available

flowchart LR
    subgraph Normal["Normal Path"]
        W[WAL Entry] --> S3[S3 Upload]
    end

    subgraph Fallback["S3 Failure"]
        W2[WAL Entry] --> SP[Disk Spillover]
        SP --> R[Retry Queue]
        R --> S3_2[S3 Upload]
    end

The spillover mechanism uses atomic writes (temp file + sync + rename) to guarantee durability even during crashes.

Follower Unavailable: WAL entries accumulate in S3 until follower recovers and catches up.

Primary Region Failure: Manual failover promotes a follower region to primary. This is a disaster recovery scenario requiring operator intervention.


Summary

ArcherDB combines proven distributed systems techniques with purpose-built geospatial optimizations:

Component Technology Why This Choice
Consensus VSR Linearizability, no log truncation, deterministic replay
Storage LSM-Tree Write optimization, sorted range scans
Spatial Index S2 Hilbert curve locality, deterministic, hierarchical
Latest Position RAM Index O(1) lookup, cache-aligned
Sharding Jump Hash Optimal resharding, zero memory overhead
Cross-Region Async Log Shipping Eventual consistency with durability

Design Trade-offs

ArcherDB makes explicit trade-offs:

  1. Memory over Disk: RAM index uses ~91GB for 1B entities - we optimize for speed, not memory efficiency
  2. Writes over Reads: LSM trees have read amplification - acceptable because location tracking is write-heavy
  3. Consistency over Availability: VSR requires quorum - we choose correctness over availability during partitions
  4. Simplicity over Flexibility: Single-purpose design - not a general-purpose database

Further Reading

Edit this page

VSR (Viewstamped Replication) Understanding

This document captures our understanding of TigerBeetle’s VSR implementation, which ArcherDB inherits. This knowledge is critical for F1 (state machine replacement) and F4 (VOPR hardening).

1. Replica Structure Overview

The Replica struct in src/vsr/replica.zig is the core consensus engine. Key fields:

Cluster Configuration

cluster: u128              - Cluster identifier
replica_count: u8          - Number of active replicas
standby_count: u8          - Number of standby nodes
replica: u8                - This replica's index
quorum_replication: u8     - Quorum size for replication
quorum_view_change: u8     - Quorum size for view changes

Protocol State

view: u32                  - Current view number
log_view: u32              - Latest view where replica became primary/backup
status: Status             - {normal, view_change, recovering, recovering_head}
op: u64                    - Latest prepared operation number
commit_min: u64            - Latest committed operation (locally executed)
commit_max: u64            - Latest committed operation (cluster-wide)

Persistent Storage

journal: Journal           - Hash-chained log of prepares (WAL)
superblock: SuperBlock     - Durable VSR state, LSM root
client_sessions            - Current client session state
client_replies             - Latest reply per client
grid: Grid                 - LSM tree storage

2. Message Flow

The Core Replication Loop

                           ┌──────────────────────────────┐
                           │         Primary              │
                           │                              │
  Client ──Request──►      │  1. Create PREPARE           │
                           │     (op, checksum, parent)   │
                           │                              │
                           │  2. Send to all replicas     │
                           └──────────┬───────────────────┘
                                      │
              ┌───────────────────────┼───────────────────────┐
              ▼                       ▼                       ▼
         ┌─────────┐            ┌─────────┐            ┌─────────┐
         │Replica 0│            │Replica 1│            │Replica 2│
         │(Primary)│            │(Backup) │            │(Backup) │
         └────┬────┘            └────┬────┘            └────┬────┘
              │                      │                      │
              │   ◄───PREPARE_OK─────┤                      │
              │   ◄───PREPARE_OK─────┼──────────────────────┤
              │                      │                      │
              │  3. Quorum reached   │                      │
              │     (commit_pipeline)│                      │
              │                      │                      │
              │──────COMMIT──────────►                      │
              │                      │                      │
              │  4. Execute on       │  5. Execute on       │
              │     state machine    │     state machine    │
              │                      │                      │
              │──────Reply───────────────────────────────────► Client

PREPARE Message

  • Sent by: Primary → All replicas
  • Contains: op, commit, view, parent checksum, client ID, operation type, body
  • Purpose: Replicate operation before committing

PREPARE_OK Message

  • Sent by: All replicas → Primary
  • Contains: prepare_checksum, op, commit_min, parent
  • Purpose: Acknowledge prepare receipt

COMMIT Message

  • Sent by: Primary → Backups (heartbeat)
  • Contains: commit number, commit_checksum, checkpoint_op
  • Purpose: Advance commit_max, detect primary liveness

3. View Changes

Status Transitions

                    ┌─────────────┐
                    │  recovering │ (startup)
                    └──────┬──────┘
                           │ journal replay complete
                           ▼
   timeout         ┌─────────────┐
 ─────────────────►│   normal    │◄──────────────────
                   └──────┬──────┘                  │
                          │ heartbeat timeout       │ received START_VIEW
                          ▼                         │
                   ┌─────────────┐                  │
                   │ view_change │──────────────────┘
                   └─────────────┘

View Change Protocol

  1. START_VIEW_CHANGE (SVC): Broadcast to all replicas, wait for quorum
  2. DO_VIEW_CHANGE (DVC): New primary collects from quorum
  3. START_VIEW (SV): New primary broadcasts to confirm new view

Quorums

  • quorum_replication: Majority required for prepare ack
  • quorum_view_change: Majority required for view change
  • Both prevent split-brain scenarios

4. State Machine Interface

Contract Methods

StateMachine {
    fn open()       // Initialize and load state
    fn commit()     // Apply operation, return reply
    fn compact()    // Run LSM compaction
    fn checkpoint() // Persist state to grid
}

Commit Stages (Primary)

idle → start → check_prepare → prefetch →
stall → reply_setup → execute →
checkpoint_durable → compact → checkpoint_data →
checkpoint_superblock → idle

Replica Events to State Machine

ReplicaEvent = union(enum) {
    message_sent,
    state_machine_opened,
    committed: { prepare, reply },
    compaction_completed,
    checkpoint_commenced,
    checkpoint_completed,
    sync_stage_changed,
    client_evicted,
}

5. Checkpoint Sequence

Checkpointing persists state durably so the journal can be truncated. The sequence ensures crash recovery correctness.

Checkpoint Flow (grid → fsync → superblock → fsync)

┌─────────────────────────────────────────────────────────────────────┐
│  1. GRID WRITE PHASE                                                │
│     - State machine writes data to grid blocks                      │
│     - LSM compaction flushes SSTables                               │
│     - Client replies persisted                                      │
│     - All writes accumulated in write buffer                        │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  2. GRID FSYNC                                                      │
│     - fsync() on grid file                                          │
│     - Ensures all data blocks durable before superblock update      │
│     - Critical: superblock must not point to non-durable data       │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  3. SUPERBLOCK WRITE                                                │
│     - Write new superblock with:                                    │
│       • checkpoint_id (monotonic)                                   │
│       • vsr_state (view, commit, etc.)                              │
│       • free_set_checksum                                           │
│       • client_sessions_checksum                                    │
│       • storage_size                                                │
│     - Superblock is written to multiple reserved sectors            │
└─────────────────────────────────┬───────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  4. SUPERBLOCK FSYNC                                                │
│     - fsync() on superblock sectors                                 │
│     - Only after this is checkpoint durable                         │
│     - Journal can now truncate up to checkpoint_op                  │
└─────────────────────────────────────────────────────────────────────┘

Checkpoint Triggers

  • Every vsr_checkpoint_ops operations (configurable)
  • After compaction completes
  • Before state sync to peer

Recovery from Checkpoint

  1. Read superblock, verify integrity
  2. Load state machine from checkpoint_id
  3. Replay journal from checkpoint_op + 1
  4. Resume normal operation

Superblock Format (src/vsr/superblock.zig)

SuperBlockHeader {
    checksum: u128,           // Self-integrity
    copy: u8,                 // Which copy (redundancy)
    version: u16,             // Format version
    cluster: u128,            // Cluster ID
    storage_size: u64,        // Total storage
    storage_size_max: u64,    // Max allowed
    sequence: u64,            // Monotonic counter
    checkpoint: Checkpoint,   // Current checkpoint state
    vsr_state: VSRState,      // Consensus state
}

6. Patterns to Reuse in ArcherDB

Keep Verbatim

  • View change protocol: Robust, well-tested
  • Quorum logic: Safety-critical
  • Hash chain verification: Integrity guarantee
  • Checkpoint coordination: Complex, working
  • Repair mechanisms: Journal, grid, state sync
  • Client session management: Deduplication

Modify for GeoEvent

  • State machine implementation: Replace legacy event structs with GeoEvent
  • Prefetch/commit logic: Adapt for GeoEvent operations
  • Reply format: GeoEvent query results

New in ArcherDB

  • S2 spatial index integration: In state machine
  • TTL expiration: Background worker + commit integration
  • Radius queries: New operation type

6. Key Invariants

  1. Op ordering: op strictly increases, hash chain enforces order
  2. Commit safety: Only commit after quorum_replication acks
  3. View monotonicity: view >= log_view >= view_durable
  4. Checkpoint bounds: Checkpoints every vsr_checkpoint_ops
  5. Primary uniqueness: One primary per view (deterministic selection)

7. File Organization

src/vsr/
├── replica.zig          - Main consensus engine
├── replica_format.zig   - Superblock format
├── journal.zig          - WAL implementation
├── superblock.zig       - Durable state management
├── clock.zig            - Logical clocks
├── free_set.zig         - Block allocation
├── grid_scrubber.zig    - Background verification
├── message_header.zig   - Protocol messages
├── multi_batch.zig      - Request batching
└── routing.zig          - Message routing

8. VOPR Relevance (F4)

VOPR (Viewstamped Replication Protocol) testing uses deterministic simulation:

  • Fault injection: Network partitions, crashes, message drops
  • Deterministic replay: Same seed = same execution
  • State verification: Check all replicas converge

For ArcherDB’s GeoEvent state machine, we’ll need:

  • GeoEvent-specific operation generators
  • S2 determinism validation
  • TTL expiration testing under faults

References

Edit this page

Durability Verification Methodology

This document describes how ArcherDB verifies its durability guarantees through comprehensive testing.

Overview

ArcherDB guarantees that committed transactions survive any single point of failure, including:

  • Process crashes (SIGKILL, SIGTERM, OOM)
  • Power loss during write operations
  • Disk failures (partial writes, bit rot, misdirected I/O)
  • Network partitions in clustered deployments

We verify these guarantees through multiple testing approaches:

Approach Coverage Platform Run Time
VOPR Simulation Consensus, WAL, Checkpoints All Hours
SIGKILL Testing Process crash recovery Linux/macOS Minutes
dm-flakey Testing Power loss, disk failures Linux only Minutes

VOPR: Viewstamped Replication Simulation

VOPR (Viewstamped Replication Optimizer and Prover) is our primary verification tool. It simulates entire ArcherDB clusters with configurable fault injection.

What VOPR Tests

  1. Consensus Protocol (VSR)

    • View changes when primary fails
    • Prepare/commit message handling
    • Quorum formation and maintenance
    • Replica synchronization
  2. WAL (Write-Ahead Log)

    • Crash during prepare phase
    • Crash during commit phase
    • Partial/torn writes
    • Journal recovery after crash
  3. Checkpoints

    • Crash during checkpoint write
    • Superblock integrity
    • State recovery from checkpoint
  4. Storage

    • Read corruption (simulated bit rot)
    • Write corruption (simulated partial writes)
    • Misdirected writes (simulated firmware bugs)
    • Crash faults (simulated torn writes)

Running VOPR

Basic verification (3-5 minutes):

./scripts/run_vopr.sh --requests-max=200

Extended verification (1-8 hours):

./scripts/run_vopr.sh --seeds "$(seq 1 100)" --requests-max=10000 --no-lite

With aggressive crash injection:

./scripts/run_vopr.sh --crash-rate=1 --requests-max=1000

Testing specific cluster configurations:

# 3-node cluster (default)
./scripts/run_vopr.sh --replicas=3

# 5-node cluster
./scripts/run_vopr.sh --replicas=5

Debugging VOPR Failures

When VOPR finds a failure, use replay mode to debug:

# Replay with full logging
./scripts/run_vopr.sh --replay <seed>

# Dump decision history on failure
./scripts/run_vopr.sh --dump-on-fail --requests-max=1000 <seed>

VOPR Fault Injection Parameters

The storage simulator supports these fault types:

Parameter Description Default Recommended for Testing
crash_fault_probability Chance of torn write on crash 0 1-5%
write_fault_probability Chance of corrupt write 0 0.1-1%
read_fault_probability Chance of corrupt read 0 0.1-1%
write_misdirect_probability Chance of misdirected write 0 0.05-0.1%

Use --crash-rate=N to set crash fault probability as a percentage.

SIGKILL Crash Testing

Tests process crash recovery by killing VOPR with SIGKILL during operation.

What It Tests

  1. Process crash recovery: ArcherDB must recover correctly after SIGKILL
  2. Deterministic behavior: Same seed must produce same results after restart
  3. No data corruption: State machine state must be consistent after recovery

Running SIGKILL Tests

# Basic test (3 iterations)
./scripts/sigkill_crash_test.sh

# Extended test
./scripts/sigkill_crash_test.sh --iterations=10 --timeout=60

# With specific seed
./scripts/sigkill_crash_test.sh --seed=12345 --requests-max=500

How It Works

  1. Start VOPR with a known seed
  2. Wait a random time (1 to --timeout seconds)
  3. Send SIGKILL to the process
  4. Restart VOPR with the same seed
  5. Verify deterministic completion (PASSED)

The test passes if VOPR can always complete successfully after being killed and restarted with the same seed.

dm-flakey Power-Loss Testing (Linux Only)

Uses Linux device-mapper dm-flakey to simulate real disk failures at the block level.

What It Tests

  1. Power loss during write: Data written but not synced
  2. Partial writes: Only part of a sector written
  3. Drop writes: Writes acknowledged but not persisted
  4. I/O errors: Disk returns errors

Prerequisites

  • Linux kernel with device-mapper (dm-flakey target)
  • Root privileges
  • At least 100MB free disk space

Running dm-flakey Tests

# Basic test (requires root)
sudo ./scripts/dm_flakey_test.sh

# Extended test
sudo ./scripts/dm_flakey_test.sh --iterations=10 --size-mb=500

How It Works

  1. Create a loop device backed by a file
  2. Create a dm-flakey device on top of the loop device
  3. Format and mount the dm-flakey device
  4. Run a real archerdb benchmark workload against a data file on that mount
  5. Trigger drop_writes during the live workload
  6. Stop the workload process group after the fault window
  7. Restore disk access and remount the filesystem
  8. Run archerdb verify on the recovered file
  9. Restart archerdb start and wait for /health/ready

macOS Alternative

dm-flakey is Linux-only. For macOS, use SIGKILL testing which provides similar (though less comprehensive) coverage.

Verification Coverage

Scenarios Tested

Scenario VOPR SIGKILL dm-flakey
Crash during prepare Yes Yes Yes
Crash during commit Yes Yes Yes
Crash during checkpoint Yes No Yes
Crash during compaction Yes No No
Multiple simultaneous crashes Yes No No
Torn writes Yes No Yes
Bit rot (read corruption) Yes No No
Misdirected writes Yes No No
Network partitions Yes No No
View changes Yes No No
Replica sync Yes No No

Scenarios NOT Tested

These scenarios are out of scope for automated testing:

  1. Full disk: Currently not simulated
  2. Kernel crash: Requires VM-based testing
  3. Hardware memory corruption: ECC testing requires special hardware
  4. Byzantine failures: VSR assumes crash-fail model, not Byzantine
  5. Multi-datacenter latency: Real network testing required

CI Integration

Pre-merge Verification

Every pull request runs:

./scripts/run_vopr.sh --seeds "$(git rev-parse HEAD)" --requests-max=200

This uses the commit hash as a seed for reproducible failures.

Extended Manual Verification

For release candidates and deep validation, run the extended verification suite manually:

# 8-hour VOPR run with swarm testing
./scripts/run_vopr.sh --no-lite --seeds "$(seq 1 1000)" --requests-max=100000

Release Verification

Before each release:

  1. 24-hour VOPR run with multiple seeds
  2. All cluster configurations (3, 5, 6 replicas)
  3. SIGKILL testing on Linux and macOS
  4. dm-flakey testing on Linux

Reproducing Failures

From CI Failure

  1. Note the seed from CI output (commit hash or explicit seed)
  2. Run locally with the same seed:
    ./scripts/run_vopr.sh --replay <seed>

From Production Issue

  1. Collect the data directory
  2. Note the replica count and configuration
  3. Run VOPR with similar parameters
  4. Use --dump-on-fail to capture decision history

Extending Coverage

Adding New Fault Types

  1. Add fault probability to src/testing/storage.zig:Options
  2. Implement fault injection in appropriate step() functions
  3. Add CLI flag in src/vopr.zig
  4. Update scripts/run_vopr.sh
  5. Document in this file

Adding New Test Scenarios

  1. Identify the scenario
  2. Determine which tool(s) can test it
  3. Implement (extend VOPR or create new script)
  4. Add to CI pipeline
  5. Document coverage

References

Edit this page

Security Best Practices

This guide documents security best practices for securing your ArcherDB deployment in a local-only environment.

Quick Security Checklist

Before deploying ArcherDB in production, verify:

Deployment Model

ArcherDB is designed for local-only deployment where security is handled at the infrastructure level. This model assumes:

  • Database runs on trusted internal networks (localhost or private VPC)
  • All clients are trusted applications on the same infrastructure
  • Physical and network security are managed at the infrastructure level
  • No direct internet exposure of database ports

This guide focuses on infrastructure-level security controls appropriate for this deployment model.

Network Security

Firewall Configuration

Block external access to ArcherDB ports:

# UFW (Ubuntu/Debian)
sudo ufw deny from any to any port 3000:3002 proto tcp
sudo ufw allow from 127.0.0.1 to any port 3000:3002 proto tcp
sudo ufw allow from 10.0.0.0/8 to any port 3000:3002 proto tcp  # Internal network

# iptables
iptables -A INPUT -p tcp --dport 3000:3002 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 3000:3002 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 3000:3002 -j DROP

Port Reference

Port Service Access
3000 Client API Internal only
3001 Replication Internal only (between replicas)
3002 Control plane Internal only
9090 Metrics (Prometheus) Monitoring infrastructure only

VPC/Private Network Deployment

For cloud deployments:

  1. Deploy in private subnet: No public IP assignment
  2. Use security groups: Allow only internal CIDR ranges
  3. Network ACLs: Block all inbound from 0.0.0.0/0 to database ports
  4. No NAT for database traffic: Database should not initiate external connections

Example AWS security group:

{
  "SecurityGroupIngress": [
    {
      "IpProtocol": "tcp",
      "FromPort": 3000,
      "ToPort": 3002,
      "SourceSecurityGroupId": "sg-app-servers"
    },
    {
      "IpProtocol": "tcp",
      "FromPort": 9090,
      "ToPort": 9090,
      "SourceSecurityGroupId": "sg-monitoring"
    }
  ]
}

No Public Internet Exposure

ArcherDB should never be directly accessible from the public internet:

  • No public IP addresses on database nodes
  • No port forwarding from public load balancers
  • No exposure through Kubernetes LoadBalancer services

If remote access is required for administration, use secure tunneling.

SSH Tunneling for Remote Access

For remote administrative access, use SSH tunneling:

# From local machine, create tunnel to remote database
ssh -L 3001:localhost:3001 user@bastion.internal

# Then connect via localhost
archerdb-cli --address 127.0.0.1:3001

For development environments:

# Forward all ArcherDB ports
ssh -L 3000:localhost:3000 -L 3001:localhost:3001 -L 3002:localhost:3002 user@dev-server

Disk Security

Full Disk Encryption

Enable full disk encryption on all volumes containing ArcherDB data:

Linux (LUKS)

# Create encrypted volume (do this before writing data)
sudo cryptsetup luksFormat /dev/sdb
sudo cryptsetup luksOpen /dev/sdb archerdb_data
sudo mkfs.ext4 /dev/mapper/archerdb_data
sudo mount /dev/mapper/archerdb_data /var/lib/archerdb

macOS (FileVault)

Enable FileVault in System Preferences > Security & Privacy > FileVault.

Windows (BitLocker)

Enable BitLocker on the data volume via Settings > Privacy & security > Device encryption.

File Permissions

Set restrictive permissions on ArcherDB data files:

# Data directory
chmod 700 /var/lib/archerdb
chown archerdb:archerdb /var/lib/archerdb

# Data files (set by ArcherDB, verify)
find /var/lib/archerdb -type f -exec chmod 600 {} \;

# Verify permissions
ls -la /var/lib/archerdb/
# Expected: -rw------- 1 archerdb archerdb

Backup Encryption

Always encrypt backups at rest:

# Encrypt platform snapshot/export with GPG
tar -C /var/lib/archerdb -cf - . | gpg --symmetric --cipher-algo AES256 > archerdb-snapshot.tar.gpg

# Encrypt with age (modern alternative)
tar -C /var/lib/archerdb -cf - . | age -r age1... > archerdb-snapshot.tar.age

For automated backups, see Backup Operations for integration with encrypted storage backends.

Secure Deletion

When decommissioning storage:

# Overwrite data files before removal
shred -vfz -n 3 /var/lib/archerdb/*.db

# Or use secure erase on SSD
hdparm --user-master u --security-set-pass SECRET /dev/sdb
hdparm --user-master u --security-erase SECRET /dev/sdb

For cloud deployments, rely on provider’s encryption and volume destruction procedures.

Security Non-Goals (Product Surface)

ArcherDB intentionally does not treat the following controls as built-in server guarantees:

  • Authentication / authorization
  • TLS / mTLS transport security
  • Native encryption-at-rest key management (the server has no data-file encryption flags; see docs/encryption-guide.md)
  • Backup scheduling/retention orchestration (continuous block upload to local/S3/GCS/Azure is built in via --backup-enabled)

These controls should be enforced in surrounding infrastructure:

  • API gateway/service mesh for authn/authz and TLS termination
  • Private networking + firewall segmentation for east-west traffic
  • Cloud/disk encryption and KMS policy controls for at-rest protection
  • Platform backup tooling (volume snapshots, object-store replication, restore drills)

Use this model as the default for production hardening and compliance evidence.

Operational Security

Server Access Control

Restrict access to ArcherDB servers:

  1. SSH key-only authentication: Disable password authentication

    # /etc/ssh/sshd_config
    PasswordAuthentication no
    PubkeyAuthentication yes
  2. Use jump boxes/bastion hosts: No direct SSH to database servers

    # ~/.ssh/config
    Host archerdb-*
      ProxyJump bastion.internal
      User archerdb-admin
  3. Principle of least privilege: Dedicated service account for ArcherDB

    # Create dedicated user
    useradd -r -s /bin/false archerdb

Log Monitoring

Monitor logs for security events:

# Watch for connection attempts
tail -f /var/log/archerdb/archerdb.log | grep -i "connection\|auth\|error"

# Monitor system logs for OOM or permission issues
journalctl -u archerdb -f

Key events to monitor:

  • Unexpected connection sources
  • Repeated connection failures
  • Permission denied errors
  • Resource exhaustion warnings

Regular Security Updates

Keep ArcherDB and system packages updated:

# Check for ArcherDB updates
archerdb --version

# Update system packages (schedule during maintenance windows)
sudo apt update && sudo apt upgrade

# Subscribe to security announcements
# https://github.com/ArcherDB-io/archerdb/security/advisories

Backup Verification

Regularly verify backup integrity:

# Verify latest snapshot/archive can be restored (quarterly)
# Example: restore in staging and run smoke checks
./scripts/test-readiness-persistence.sh

# Test actual restore to staging environment (monthly)
# See disaster-recovery.md for full procedure

Security Assumptions

This deployment model is appropriate only if these assumptions hold:

Assumption Verification
Network isolation Database ports not reachable from untrusted networks
Client trust All connecting applications are vetted and trusted
Physical security Server room/cloud account access is controlled
OS-level security Firewall active, disk encryption enabled, patches applied
Single-tenant No multi-tenant isolation requirements

If any assumption does not hold, do not expose ArcherDB directly. Introduce an external security boundary (gateway/proxy/service mesh) and infrastructure encryption controls before expanding trust boundaries.

When to Add External Controls

Add or strengthen external controls when:

  • Remote access is required (non-local clients)
  • Multi-tenant isolation is required
  • Compliance controls require auditable authn/authz and encrypted transport
  • Data sensitivity requires defense in depth
  • Any workload introduces untrusted clients or networks
Edit this page

ArcherDB Data Protection Guide

ArcherDB uses an infrastructure-managed protection model: platform-enforced encryption at rest is the supported control for production deployments.

Scope

The archerdb start path has no data-file encryption flags. Earlier builds parsed --encryption-enabled/--encryption-key-* without wiring them into the storage path; those flags were removed so an operator cannot believe at-rest encryption is active when it is not.

What does exist, as tooling rather than a server-side at-rest control:

  • An encrypted-file format library (AES-256-GCM/Aegis-256, per-file DEKs wrapped by a KEK, file/AWS KMS/Vault key-provider interfaces) in src/encryption.zig, exercised by unit and integration tests
  • archerdb verify --encryption, which detects the ARCE encrypted-file header and verifies headers/DEK unwrap/GCM tags on files that carry it

Not provided:

  • Server-side encryption of live data files
  • Key lifecycle orchestration (rotation schedules, escrow, revocation workflows)

Use external controls as the load-bearing at-rest protection.

1) Encrypt Storage Volumes

Use the native encryption mechanism of your environment:

  • Cloud block volumes with provider-managed keys (KMS-backed)
  • LUKS/FileVault/BitLocker for self-managed hosts
  • Encrypted object storage for snapshots/archives

2) Centralize Key Management

Manage keys in dedicated key systems:

  • Cloud KMS
  • HSM-backed key services
  • Vault-based key governance

3) Enforce Access Controls

  • Restrict node and storage IAM roles
  • Separate key administrators from DB operators
  • Audit all key access and policy changes

4) Verify Encryption Continuously

  • Validate encrypted volume settings in IaC/CI
  • Alert on unencrypted volumes and buckets
  • Run periodic restore drills from encrypted snapshots

Example Verification Checklist

Migration Note

If you previously passed --encryption-enabled or --encryption-key-* to archerdb start (experimental builds), remove them: they were never wired into the storage path and the server now rejects them as unknown flags. Encryption-at-rest is a platform requirement around ArcherDB.

Edit this page

ArcherDB Data Protection Security Model

This document defines ArcherDB’s security boundary for data protection.

Security Boundary

ArcherDB is designed for trusted-network deployment and assumes:

  • Authentication and authorization are enforced before ArcherDB (gateway/service layer)
  • Transport security is enforced outside ArcherDB (TLS termination/service mesh/private links)
  • At-rest encryption is enforced by storage and cloud platform controls
  • Backup confidentiality and retention are enforced by external backup tooling

In Scope (ArcherDB)

  • Replication safety and durability guarantees
  • Deterministic recovery behavior (recover, replica sync)
  • Operational guidance for private-network deployment

Out of Scope (ArcherDB Product Surface)

  • Native authn/authz provider implementation
  • Native TLS/mTLS session management for client and replica traffic
  • Native encryption-at-rest and key hierarchy lifecycle
  • Native backup orchestration and encrypted archive lifecycle

Required External Controls

For production deployments, implement:

  • API gateway/service mesh policies for identity and authorization
  • Network segmentation + firewall controls for DB ports
  • Encrypted storage volumes and encrypted snapshot/object stores
  • KMS/HSM-backed key governance and audit logging
  • Backup/restore automation with periodic drill evidence

Compliance Positioning

Compliance attestations (SOC 2, HIPAA, PCI) should map to platform and organizational controls around ArcherDB, not to built-in ArcherDB cryptographic/auth subsystems.

Operator Checklist

Edit this page

Message Bus Error Handling

This document explains how the message bus classifies and handles errors.

Error Classification Principles

The message bus uses a classified error handling approach rather than terminating all connections on any error. This reduces unnecessary reconnections while still maintaining correctness and security.

Core principles:

  1. Protocol violations are always fatal (security boundary)
  2. Peer-initiated disconnects are normal (not errors)
  3. Resource exhaustion rejects new work, keeps existing connections
  4. Timeouts are configurable (currently hardcoded, future work)

Error Categories

Fatal Errors (Terminate Connection)

These errors indicate the connection is unusable or untrustworthy:

Error Rationale
Protocol violation Peer sent invalid data, cannot trust further communication
Version mismatch Incompatible protocol versions
Authentication failure Peer failed to authenticate
Malformed message Header/payload corruption or invalid format
Misdirected message Message peer type doesn’t match connection type

Action: Terminate connection immediately with shutdown().

Peer-Initiated Disconnects (Not Errors)

These indicate the peer closed the connection - normal operation:

Error Rationale
ConnectionResetByPeer Peer closed connection (orderly or crash)
BrokenPipe Write to closed connection (send path)
Zero bytes received Orderly shutdown signal

Action: Log at info level, terminate without shutdown (peer already gone).

Timeout Errors (Configurable)

These indicate the operation took too long:

Error Rationale
WouldBlock Operation would block (timeout reached)
ConnectionTimedOut TCP-level timeout

Action: Currently terminate with warning log. Future: make configurable.

Resource Exhaustion (Reject New Work)

These errors indicate system capacity limits:

Error Rationale Accept Recv/Send
SystemResources OS resource limit (memory, buffers) Yes Yes
ProcessFdQuotaExceeded Process file descriptor limit Yes No
SystemFdQuotaExceeded System file descriptor limit Yes No

Action for accept: Log at WARN, reject new connection, continue accepting. The OS will backpressure by queueing in the listen backlog.

Action for recv/send: Terminate connection (cannot complete I/O).

Operator response: Increase limits or add capacity.

Transient Errors (Log and Continue)

These errors may occur during normal operation and don’t indicate problems:

Error Rationale
ConnectionAborted Connection aborted before accept completed

Action: Log at debug level, continue normal operation.

Peer Eviction

When connection slots are exhausted and a replica needs to connect:

  1. Prefer dropping client connections over unknown connections
  2. Prefer dropping unknown connections over replica connections
  3. Log at WARN level with evicted peer type
  4. Future: emit metric for alerting
  5. Future: emit cluster event for operator automation

Rationale: Replica-to-replica connections are critical for consensus. Client connections can reconnect. Unknown connections haven’t proven their identity yet.

Platform Differences

shutdown() Behavior

The shutdown(fd, SHUT_RDWR) syscall signals graceful close intent:

Platform Behavior
Linux Pending I/O operations return immediately with error
Darwin Similar behavior, but timing may differ slightly

Both platforms support graceful close via shutdown(.both) before close().

SocketNotConnected on shutdown()

This error can occur in several benign scenarios:

  1. Terminating during in-progress connect operation
  2. Peer closed connection before we initiated shutdown
  3. Connection failed during establishment

All cases are handled by continuing with termination cleanup.

Connection State Machine

free -> accepting -> connected -> terminating -> free
         |              ^
         v              |
        error ------> free
         |
free -> connecting -> connected -> terminating -> free
         |              ^
         v              |
        error ------> terminating

States:

  • free: Connection slot available for reuse
  • accepting: Reserved for in-progress accept operation (inbound)
  • connecting: Outbound connection in progress (to replica)
  • connected: Fully established, may recv/send
  • terminating: Cleanup in progress, waiting for I/O completion

Valid Transitions (all guarded by assertions):

From To Guard Location
free accepting connection.state == .free accept()
accepting connected assert(state == .accepting) accept_callback success
accepting free assert(state == .accepting) accept_callback error
free connecting assert(state == .free) connect_connection()
connecting connected assert(state == .connecting) connect_callback success
connecting terminating via terminate() connect_callback error
connected terminating assert(state != .terminating && state != .free) terminate()
terminating free assert(state == .terminating) terminate_close_callback

Invariants:

  • No double-termination: assert(connection.state != .terminating) before terminate
  • No terminating free connections: assert(connection.state != .free) before terminate
  • Orderly cleanup: terminating state waits for pending I/O before closing fd
  • Re-initialization: Connection struct reset to defaults on close (state = .free)

Configuration (Future Work)

Currently, timeout behavior is hardcoded in constants.zig:

  • connection_delay_min_ms / connection_delay_max_ms: Reconnect backoff
  • tcp_keepidle, tcp_keepintvl, tcp_keepcnt: TCP keepalive
  • tcp_user_timeout_ms: TCP user timeout

Future improvements:

  • Configurable idle timeout (how long inactive connection stays open)
  • Configurable read/write timeout (per-operation timeout)
  • Retry policy configuration (exponential backoff parameters)
  • Per-connection timeout overrides

Logging Levels

Level When Used
debug Transient errors during accept, shutdown errors during termination
info Peer-initiated disconnects, connection establishment
warn Resource exhaustion, peer eviction, timeouts, unexpected errors
err Fatal system errors (e.g., cannot create socket)

Metrics (Future Work)

Planned metrics for observability:

  • message_bus_accept_errors_total{type="resource_exhaustion|transient|other"}
  • message_bus_peer_evictions_total{peer_type="client|unknown"}
  • message_bus_connection_terminations_total{reason="peer_close|timeout|error"}
  • message_bus_connections_active{peer_type="replica|client|unknown"}
Edit this page

Upstream TigerBeetle Backport Ledger

ArcherDB’s VSR/LSM/IO core derives from TigerBeetle. This ledger tracks the fork lineage and the disposition of every upstream change we have reviewed, so backporting is a repeatable process with explicit verdicts rather than a one-off sweep.

  • Fork point: TigerBeetle commit 0baa07d3 (2025-12-29) — verified by byte-identical src/vsr/replica.zig blob at ArcherDB’s import commit f77fa3cf.
  • Reviewed through: TigerBeetle 4abc0229a = release 0.17.7 (2026-06-12); 1056 upstream commits, ~470 touching shared subsystems; 147 curated changelog entries across releases 0.16.68–0.17.7 triaged.

Process rules:

  • Every ported hunk carries the upstream PR number and SHA in a code comment.
  • Consensus-path ports require a local VOPR campaign (both state machines, multiple seeds) before merge, on top of the unit/integration battery.
  • Wire-format-adjacent changes (message headers, on-disk layout) are never bundled with routine ports; they get their own change with compat analysis.

Ported (2026-06-12 batch)

Upstream What ArcherDB location
PR #3726 (44865e796) CheckpointTrailer.open bounds assertion (security-audit): corrupt trailer_size must not index past block arrays src/vsr/checkpoint_trailer.zig
PR #3717 (b1b968b4d) IO event-listener crash: non-nullable completion context; event_listen passed undefined into an @alignCast trampoline (latent UB) src/io/linux.zig
PR #3693 (bba77d0c0) File-creation permissions: data file/probe 0o666→0o600; executables 0o777→0o755 src/io/linux.zig, src/shell.zig, src/multiversion.zig, src/build_multiversion.zig
PR #3729 (1c17caa1c) AMQP method decode fails closed on invalid enum (@enumFromInt UB on broker-controlled bytes → intToEnum catch error.Unexpected); generator script patched; embedded-spec checksum guard updated src/cdc/amqp/spec.zig, spec_parser.py, amqp.zig
PR #3704 (4e275db15, behavioral core only) Prepare-timeout retries fan out to every replica missing from the ack set, instead of cycling one candidate per timeout (tail latency / liveness under partial partitions). Ported without the star-replication doc rewrite it shipped with src/vsr/replica.zig on_prepare_timeout

Already present before this batch (no action): clock skew warn threshold at 50ms (src/vsr/clock.zig:540 matches upstream 0d2f7323c).

Staged — phase 2 (medium, individually portable)

Upstream What Notes
PR #3701 (983ba27d4) Reformat reentrancy: IO.run invoked from within IO.run_for_ns Pattern present in src/vsr/replica_reformat.zig; M-size
PR #3748 (2875a188a) Accept-path makes room for replica connections (evict hogging clients/unknown peers) src/message_bus.zig + constants; M-size
PR #3769 (f3fa72c70) Client pings during registration; eviction reply on ping Touches message_header.zig — wire-adjacent, needs compat pass
e1082a878 Per-replica budget for block repair Depends on repair_budget structure; verify our copy’s lineage first
PR #3592 (0d7fd8953) DISCARD on block-device format (SSD perf) Small; block-device path only
PR #3780 (e7c4e454b) Ban std.fmt.parseInt in protocol paths (tidy rule + sites) L but mechanical; aligns with our parser-honesty stance

Staged — phase 3 (large chains; require dedicated VOPR campaigns)

  1. Grid block ownership via reference counting (PR #3686, 52e149b75, ~400 lines) — correctness-critical lifetime management; prerequisite for the compaction work. Complicated by ArcherDB’s radix_buffer scratch and geo grooves.
  2. Tombstone semantics on the lookup path (PR #3735 chain: 81f7e49d0, 8a8d77bb3, 8116232a7, …) — explicit tombstone variants + fuzzing.
  3. Incremental compaction merge (PRs #3673 + #3770, ~930 lines) — the headline upstream tail-latency win (merge spread across the bar instead of one big end-of-bar merge). Must integrate with ArcherDB’s compaction_throttle, compression, and TTL-expiry accounting. Port order: (1) → (2) → (3).
  4. Linux IO event-loop refactor (PR #3619, a6ec07462) — timeout embedded in io_uring_enter, next_tick as top-level; upstream measured ~8% on their standard benchmark plus tail-latency gains. Large; our io/linux.zig has local modifications to merge through.
  5. Star replication routing (PRs #3669/#3668) — strategy change (ring-forward → primary broadcast). Evaluate deliberately: changes bandwidth/latency trade-offs; we kept the fork-era routing and ported only the #3704 retry fan-out which is strategy-independent.

Rejected / not applicable

Upstream Verdict
PR #3529 (5d934474e) key_range_contains fastpath N/A as-is — would assert-crash. Upstream’s fix assumes the post-fork caller convention (concrete prefetch snapshots, assert(snapshot < snapshot_latest)); ArcherDB still passes the snapshot_latest sentinel (src/lsm/groove.zig:830), under which our existing code already takes the fast path. Revisit only together with the snapshot-convention migration.
TB accounting state-machine changes (src/state_machine.zig, transfers logic) ArcherDB uses its own geo state machine
TB REPL parser overhaul (PR #3786) Our REPL diverged; evaluate separately if parser issues surface
TB client-library fixes (.NET/Java/Go, PRs #3778/#3762 leak+overflow) Our SDKs diverged at fork but shared ancestry — investigate whether the leak/overflow patterns exist in our Java/Go clients before dismissing
Release/CI tooling (their wheel builder, release validation serialization) We have our own (and already build Python wheels without hatchling-equivalent issues post our packaging fix)
Edit this page