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.
Quick Links
| 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:
- SDK Retry Semantics - Configure retry behavior and handle errors
- Error Codes - Understand and troubleshoot errors
- Testing Guide - Run tests locally for all 5 SDKs
- Benchmark Guide - Run and interpret performance benchmarks
- Release Checklist - Must-do go/no-go checklist before calling ArcherDB released
- Java Publish Checklist - Separate maintainer checklist for Maven Central publication
Reference
Complete API and configuration documentation:
- API Reference - All operations with request/response details
- OpenAPI Specification - Machine-readable API definition
- Tier Profiles - Runtime/capacity presets and release artifact guidance
- Hardware Requirements - Minimum and recommended specs
- LSM Tuning - Storage engine configuration
- Journal Sizing - Write-ahead log configuration
SDK Documentation
Comprehensive guides for each language:
- SDK Overview - Choosing an SDK, feature matrix, common patterns
- SDK Comparison Matrix - Feature parity and code examples
- SDK Limitations - Known issues and workarounds
- Parity Matrix - Cross-SDK verification status
- SDK Comprehensive Test Report - Current SDK validation evidence
| 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
- Operations Runbook - Day-to-day operational procedures
- Capacity Planning - Sizing clusters for your workload
- Multi-Region Deployment - Design/reference guide; server runtime is not GA
Recovery & Continuity
- Backup Operations - External snapshot/backup integration guidance
- Disaster Recovery - Replica recovery and external restore procedures
- Upgrade Guide - Rolling upgrades and external rollback planning
Troubleshooting
- Troubleshooting Guide - Diagnose and resolve common issues
- Error Codes - Error reference with troubleshooting guidance
Alert Runbooks
Per-alert response guides linked from Prometheus alerts:
- Replica Down - When a replica is unreachable
- View Changes - Frequent leader elections
- Index Degraded - Index performance issues
- High Read Latency - Read latency triage
- High Write Latency - Write latency triage
- Disk Capacity - Disk capacity and fill-rate response
- Compaction Backlog - Storage compaction backlog response
- Kernel Crash Durability - Crash-durability harness guidance
Performance
- Benchmarks - Benchmark framework and results
- Benchmark Guide - Running and interpreting benchmarks
- Profiling - Performance profiling workflows
- Performance Tuning - Query and workload optimization
- LSM Tuning - Storage engine optimization
Testing & CI
- Testing Guide - Run all tests locally
- CI Tiers - Smoke, PR, nightly, weekly tiers
- curl Examples - Raw HTTP examples for all operations
- Protocol Reference - Wire format and data types
Understanding ArcherDB
Architecture and design:
- Architecture - System design, data flow, and component interactions
- VSR Understanding - How Viewstamped Replication provides consensus
- Durability Verification - How ArcherDB ensures data durability
Security
- Security Best Practices - Infrastructure security and trust-boundary model
- Encryption Guide - External encryption-at-rest controls
- Encryption Security - Data-protection model and non-goals
Internals
For contributors:
- Message Bus Errors - Network layer error handling
- Upstream Backports - Local patches carried from upstream dependencies
Release Notes
CHANGELOG.md- Release history and notable changesFINALIZATION_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.
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.archerdbYou 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 archerdbNode.js
npm install archerdb-nodeGo
go get github.com/ArcherDB-io/archerdb/src/clients/goJava (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
- Getting Started Guide - Batching, polygon queries, error handling
- API Reference - Complete operation documentation
- Operations Runbook - Production deployment
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 --versionmacOS (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 --versionmacOS (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 --versionBuild from Source
git clone https://github.com/ArcherDB-io/archerdb.git
cd archerdb
./zig/download.sh
./zig/zig build
# Binary at ./zig-out/bin/archerdbChoose 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, ultraStarting 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.archerdbYou 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.dbSDK Installation (~1 min)
Python
pip install archerdbNode.js
npm install archerdb-nodeGo
go get github.com/ArcherDB-io/archerdb/src/clients/goJava (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.archerdbS3 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 timeoutConfiguration 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 pageSDK 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 = trueBackoff 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 outview_change_in_progress- Leader election in progressnot_primary- Connected to non-primary replicacluster_unavailable- No quorum availablesession_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 rangepolygon_too_complex- Too many verticesbatch_too_large- Batch exceeds maximum sizequery_result_too_large- Query limit exceededinvalid_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 recursivelyGo
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:
- SDK uses the same
request_numberfor all retry attempts - If the request already executed, server returns the cached response
- 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
Use upsert over insert - Upsert operations are idempotent and safe to retry.
Keep batches reasonably sized - Smaller batches (500-1000 events) have better retry characteristics than maximum-sized batches.
Handle RetryExhausted - Always catch retry exhaustion and implement application-level fallback.
Use split_batch for large imports - When importing large datasets, proactively split into chunks rather than waiting for timeouts.
Monitor retry metrics - Track retry counts in production to detect cluster issues early.
Don’t disable retry without reason - The default retry configuration handles most transient failures automatically.
Related Documentation
- Source:
src/error_codes.zig
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 errorJava
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 |
Related Documentation
- SDK Retry Semantics - Detailed retry configuration
- Disaster Recovery - Recovery procedures
- Operations Runbook - Operational procedures
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 build2. 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 30013. 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/ -vNode.js:
cd src/clients/node
npm install
npm testGo:
cd src/clients/go
go test ./... -vJava:
cd src/clients/java
mvn testC:
cd src/clients/c
make testServer (Zig unit tests):
./zig/zig build -j4 -Dconfig=lite test:unit4. 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 integrationParity 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 -vResults are written to:
reports/parity.json- Machine-readabledocs/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
Check if binary exists:
ls zig-out/bin/archerdbBuild if missing:
./zig/zig build -j4 -Dconfig=liteCheck for port conflicts:
lsof -i :3001
Tests Fail with Connection Errors
Verify server is running:
curl http://127.0.0.1:3001/ping # Should return: {"pong":true}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:unitPreserving 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 checkCI 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
- CI Tier Structure - CI pipeline organization
- Performance Baselines - Regression thresholds and benchmark baselines
- Benchmark Guide - Performance testing
- SDK Comparison Matrix - SDK feature parity
- SDK Limitations - Known issues and workarounds
- Parity Matrix - Cross-SDK verification status
Last updated: 2026-02-01
Edit this pageCI 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=60PR:
./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300Nightly (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 stopWeekly (requires cluster setup):
python3 test_infrastructure/benchmarks/cli.py run --full-suiteWorkflow 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
- Testing Guide - Local test running
- Benchmark Guide - Performance testing details
- Parity Matrix - SDK verification status
Last updated: 2026-02-01
Edit this pagePerformance 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
- Main branch pushes upload new baseline artifact (full benchmark mode)
- PRs download the current main baseline and run quick benchmarks
- Comparison checks both throughput and P99 latency against thresholds
- 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
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}
Merge your PR (no comparison runs without baseline)
New baseline created on next main push
Alternative: Update Baseline Manually
If you don’t want to delete the artifact:
- Merge to main (workflow runs)
- New baseline automatically uploaded
- 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:
- Review recent commits for performance-impacting changes
- Profile locally to identify hot paths
- Fix the performance issue
- 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 bcConfiguration
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:
- Edit the multipliers in
benchmark-ci.sh - Update this documentation
- Update workflow header comments
Benchmark Modes
| Mode | Duration | Use Case |
|---|---|---|
| quick | ~30 seconds | PRs (fast feedback) |
| full | ~5 minutes | Main branch (accurate baseline) |
Related Files
.github/workflows/benchmark.yml- CI workflowscripts/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
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=liteQuick 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 10000Full 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-mixedMixed 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.0Compare to Baseline
# Compare current run to stored baseline
python3 test_infrastructure/benchmarks/cli.py compare baseline.json current.jsonInterpreting 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:
- Spins up clusters (1/3/5/6 nodes)
- Runs full benchmark suite
- Compares to baseline
- Alerts on >10% regression
- Can promote approved results into
benchmarks/history/ - 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.ymlProgrammatic 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
- Single-Node Evidence, August 2026 - Measured ArcherDB vs Valkey vs PostGIS comparison with reproduction steps
- Detailed Benchmark Framework - Statistical methodology
- Testing Guide - Running tests locally
- CI Tiers - Weekly benchmark workflow
- Performance Tuning - Optimization guidance
Last updated: 2026-02-01
Edit this pageSingle-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 useappendonly yes+appendfsync always. - PostGIS 16-3.4: official docker image, schema + GIST index from
scripts/competitor-benchmarks/setup-postgis.sh,psycopg2.execute_valuesbatched 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 --jsonCaveats: 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).
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:
CISDK Smoke TestsPerformance 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.mdso its date and commit match the release candidate
- regenerate
- confirm the current GA boundary is still truthful in code and docs:
clusteris status-onlyupgradeis status plus dry-run planning only- offline
shard reshardremains 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.mdbeyond 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-javato 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
GOmeans the exact release commit is selected, green, evidence is refreshed to that commit, and the current GA boundary is still truthful.NO-GOmeans 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/maincommit is green forCI,SDK Smoke Tests, andPerformance 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
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
mainor 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.mdorFINALIZATION_PLAN.mdstill 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:
CISDK Smoke TestsPerformance Benchmarks
- docs/PARITY.md and
reports/parity.jsonare current and green for the candidate commit - docs/BENCHMARKS.md still matches the current checked-in benchmark artifact set
integration-check-report.mdsays the only remaining release-evidence work is the external Central rehearsal- the Java release build stages all four artifacts:
archerdb-java-<version>.jararcherdb-java-<version>-sources.jararcherdb-java-<version>-javadoc.jararcherdb-java-<version>.pom
- the release host has:
- Java
- Maven
MAVEN_USERNAMEMAVEN_CENTRAL_TOKENMAVEN_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 HEADBuild the scripts binary once:
./zig/zig build scripts:build -j2Set 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 --buildConfirm the staged files exist:
ls -1 zig-out/dist/javaSet 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 LONGRun the no-side-effects publish preflight:
./zig-out/bin/scripts release --sha=<commit> --language=java --publish --preflightOnly if every earlier gate is green, do the real publish:
./zig-out/bin/scripts release --sha=<commit> --language=java --publishDecision Rule
GOmeans every repo-side gate is green, the candidate commit is intentionally being released, and the publish preflight passes on the release host.NO-GOmeans anything else. In that case, do not publish to Maven Central yet.
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
- Client sends a request to any replica
- If the replica is not the primary, it forwards to the primary
- Primary replicates to followers and waits for quorum
- 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,000lon_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.cursorGo
// 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-446655440000SDK 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.errorfor 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 leader222- 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-000000000001Response:
{
"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
limitof 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 failsWhen 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 leader222- Resharding in progress- Network timeouts
Non-retryable errors (fix request first):
100- Invalid coordinates101- Invalid entity ID300- 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
- Client connects to any replica
- Client sends
Registermessage with cluster ID - Server responds with session ID
- 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.
Related Documentation
- Getting Started - Tutorial with complete examples
- Error Codes - Complete error reference
- SDK Retry Semantics - Retry configuration
- OpenAPI Specification - Machine-readable API spec
SDK Documentation
src/clients/python/README.md- Python client librarysrc/clients/node/README.md- Node.js/TypeScript client librarysrc/clients/go/README.md- Go client librarysrc/clients/java/README.md- Java client librarysrc/clients/c/README.md- C client library
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:
litestandardproenterpriseultra
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
litetoultra. - 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
litethe default recommendation for demos/evaluation. - Build release artifacts with performance-appropriate optimization settings for the release target.
Change Checklist (When Editing Tier Defaults)
- Confirm standard–ultra tiers still share the same runtime/performance knobs.
- Confirm lite uses the lite runtime; standard–ultra use the high-perf runtime.
- Confirm only RAM/disk quotas differ across tiers within each runtime class.
- Verify tier ordering remains monotonic by capacity
(
lite->standard->pro->enterprise->ultra). - Update this document and linked user-facing docs in the same change.
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
- Quick Reference
- Minimum Requirements
- Recommended Specifications
- Memory Sizing
- Storage Sizing
- Network Requirements
- Cloud Instance Mapping
- Sizing Calculator
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 |
Recommended Specifications
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: $____________
Related Documentation
- Capacity Planning - Detailed capacity planning guide
- Benchmarks - Performance benchmark results
- Operations Runbook - Deployment and tuning
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
- RAM index pressure (
- 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=100Maintained benchmark harness
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60Capacity test (real run)
python3 scripts/test_capacity_limits.py --config lite --optimize ReleaseFast
python3 scripts/test_capacity_limits.py --config standard --optimize ReleaseFastBy default, capacity test artifacts are written to
/tmp/archerdb_capacity_runs.
Interpreting Results
For capacity runs, verify:
- No early transport bottleneck (
status=1) at normal batch sizes. - Throughput remains in the same order of magnitude across tiers on the same hardware.
- Failure reason changes with capacity quotas, not tier runtime behavior.
Example summary fields:
events_insertedunique_entriescpu_percent_avg/cpu_percent_peakram_rss_avg_bytes/ram_rss_peak_bytesdisk_logical_bytes/disk_physical_bytes_from_dufailure_reasonandfirst_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-sizecan lower RAM index budget at runtime but cannot exceed the tier cap.- Increase
ram_index_size_defaultonly if product intent requires a higher capacity boundary.
Capacity run fails on storage limit
- Verify
storage_size_limit_defaultandstorage_size_limit_maxfor the selected tier. - Use larger tier quotas when the workload requires longer retention.
References
src/config.zigsrc/constants.zigscripts/test_capacity_limits.pyzig-out/bin/archerdb benchmarktest_infrastructure/benchmarks/cli.py
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_maxto 4 MiB (32K events per batch) - Balance: retention time vs. batch latency vs. memory
Key Files
src/constants.zig: Derived configuration valuessrc/config.zig: Base configurationsrc/vsr/journal.zig: WAL implementationsrc/vsr.zig: Checkpoint logic
References
- Source:
src/vsr.zig
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.
- Platform Support — what each SDK actually ships and validates per OS/arch
- SDK Comparison Matrix — parity and ergonomics across language clients
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 onlytb_client.dll/tb_client.lib— misnamed TigerBeetle-era leftovers that nothing could link aslibarch_client(removed 2026-06-12;zig build clients:cnever 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: awindowstarget in the relevantzig 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.
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.
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.
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
BigIntfor 64-bit and 128-bit values. Callers must preserveBigIntsemantics end-to-end. - Build and test flows depend on native bindings and a working Node.js toolchain.
Go
- Uses ArcherDB-specific types such as
Uint128and 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
NEARESTrouting: a backgroundLatencyProberperiodically TCP-connects to each configured region, maintains rolling RTT averages per region, and selection picks the healthy region with the lowest average.FOLLOWERrouting 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 pageSDK 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
- Direct comparison between all SDKs
- Python SDK as golden reference for tie-breaking
- 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 goCI Integration
Machine-readable report: reports/parity.json
Last updated: 2026-06-12T03:27:03.820843Z
Edit this pageArcherDB 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:
- docs/PARITY.md
reports/parity.json
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
Operations Runbook
This runbook provides operational procedures for running ArcherDB in production.
Table of Contents
- Cluster Management
- Monitoring
- Alerting
- Alert Response Guides
- Scaling
- Kubernetes Deployment
- Upgrade Procedures
- Maintenance
- Troubleshooting
- Emergency Procedures
- Related Documentation
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.dbProduction 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.dbStopping a Cluster
Graceful Shutdown:
# Send SIGTERM to each replica
kill -TERM $(pidof archerdb)
# Or use systemd
systemctl stop archerdbOrder 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:3000Cluster 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=12345Notes:
- The public
archerdb clusterCLI currently supportsstatusonly; membership mutation remains an external orchestration concern. - Cluster membership is fixed by the startup
--addressesset. - 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=60Monitoring
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: 15sGrafana 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:
- Check alert severity - Critical alerts need immediate attention
- Open the runbook - Click the runbook_url in the alert annotation
- Follow Immediate Actions - Complete the checklist in order
- Investigate - Use diagnostic commands to identify root cause
- Resolve - Follow resolution steps for the identified cause
- 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.dbTrade-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.dbNote: Read replicas don’t participate in consensus and may have slight lag.
Client-Side Scaling
- Connection pooling: Use
pool_size > 1for 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
kubectlconfigured with cluster access- A StorageClass supporting
ReadWriteOncevolumes (e.g.,gp3on AWS,pd-ssdon GCP) - Network policy allowing inter-pod communication on port 3000
StatefulSet Deployment
Create a namespace and deploy the ArcherDB cluster:
kubectl create namespace archerdbConfigMap 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: archerdbStatefulSet 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: 10GiDeploy the manifests:
kubectl apply -f archerdb-config.yaml
kubectl apply -f archerdb-headless.yaml
kubectl apply -f archerdb-statefulset.yamlVerification
# 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_roleClient 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: archerdbClients 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"
done2. 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
done3. 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 error4. 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 archerdbKubernetes 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 primaryPost-Upgrade Verification
After all replicas upgraded:
Rollback Procedure
If issues occur during upgrade:
1. Stop the problematic replica:
systemctl stop archerdb2. Restore the old binary:
mv /usr/local/bin/archerdb.old /usr/local/bin/archerdb3. Start with old version:
systemctl start archerdb4. For Kubernetes rollback with your deployment tooling:
kubectl rollout undo statefulset/archerdb -n archerdbIf 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.dbPerformance 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.csvThe 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 certificatesTroubleshooting
This section covers quick troubleshooting tips. For comprehensive diagnosis and resolution procedures, see the Troubleshooting Guide.
Connection Issues
Symptom: Clients can’t connect
Checklist:
- Check server is running:
systemctl status archerdb - Check port is open:
netstat -tlnp | grep 3000 - Check firewall rules:
iptables -L -n - Check gateway/proxy TLS certificates (if applicable)
- Verify cluster_id matches between client and server
High Latency
Symptom: P99 latency > 100ms
Checklist:
- Check disk I/O:
iostat -x 1 - Check for view changes:
curl localhost:9090/metrics | grep view_changes - Check replication lag:
curl localhost:9090/metrics | grep replication_lag - Check batch sizes (too small = overhead, too large = queuing)
- Check if compaction is running
Cluster Won’t Start
Symptom: Replicas won’t form quorum
Checklist:
- Verify all replicas use same cluster_id
- Check network connectivity between nodes
- Check for clock skew:
chronyc tracking - Verify data files aren’t corrupted:
./archerdb verify - Check logs for specific errors
Out of Disk Space
Symptom: Writes failing with “out of space”
Immediate Actions:
- Check disk usage:
df -h - Identify largest files:
du -sh /data/* - If TTL is configured, wait for expiration
- Run compaction:
./archerdb compact - 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 oneRecovering 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 replicasEmergency 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 loggingContact 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).promRelated Documentation
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.archerdbS3 / 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 |
Capacity Planning Guide
This guide helps you size ArcherDB deployments for your expected workload.
Table of Contents
- Quick Reference
- Memory Planning
- Disk Planning
- Hardware Recommendations
- Scaling Scenarios
- Monitoring Capacity
- Growth Planning
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_hugepagesLarge 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:
Current vs. Maximum Capacity
- Entity count / max entities
- Disk used / disk available
- Memory used / memory available
Growth Trends
- Entity count over 30 days
- Disk growth rate (GB/day)
- Event ingestion rate
Resource Efficiency
- Index load factor
- Query latency trends
- Compaction throughput
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)
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 3Future 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 followerReading 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:
- Stop writes to primary region
- Wait for followers to catch up (lag → 0)
- Promote follower to primary:
./archerdb promote --cluster=12345 --addresses=10.0.2.1:3001 - Reconfigure old primary as follower
- Update client configuration with new primary
Unplanned Failover
If primary region fails:
Identify most caught-up follower:
./archerdb status --cluster=12345 --addresses=10.0.2.1:3001 # Check commit_op to find most advanced followerForce promote the best follower:
./archerdb promote --force --cluster=12345 --addresses=10.0.2.1:3001Warning: Force promotion may lose operations not yet replicated.
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
passBest Practices
- Region Selection: Place primary in the region with most write traffic
- Follower Count: 1-3 followers per region (more increases replication load)
- Network: Use dedicated inter-region links or VPN for replication
- Monitoring: Alert on
replication_lag_seconds > 5 - Backups: Run external snapshot pipelines from the primary region
- Testing: Regularly test failover procedures
Troubleshooting
High Replication Lag
- Check network latency between regions
- Verify follower has sufficient CPU/IO capacity
- Consider S3 transport for high-latency links
- Check
ship_queue_depthmetric for backpressure
Follower Not Receiving Updates
- Verify
--primary-regionendpoint is correct - Check firewall allows TCP 3000/3001 between regions
- Check primary logs for shipping errors
- Verify S3 bucket permissions (if using S3 transport)
Related Documentation
- Error Codes Reference - Multi-region error codes
- Disaster Recovery - External snapshot recovery procedures
- Operations Runbook - Day-to-day operations
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:
- Replica redundancy for high availability
- Built-in backup upload (S3/GCS/Azure/local) for durable off-host copies
- Volume/object snapshots or cross-region immutable copies for defense-in-depth
- Off-site retention lock for regulated workloads
Recommended Backup Sources
- 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)
- Confirm cluster health and quorum
- Trigger platform snapshot for all replica data volumes
- Replicate snapshots/artifacts to secondary region/account
- Record snapshot IDs, timestamps, and checksums in runbook
Restore Procedure (Generic)
- Provision replacement nodes/volumes
- Restore data volumes from selected snapshot set
- Start replicas and rejoin cluster using standard startup/recover flow
- 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
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
- Single replica loss (quorum remains)
- Minority replica loss (quorum remains)
- Majority loss (quorum lost)
- Full cluster loss
- 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.archerdbThen start the replica normally and allow it to catch up.
Full Cluster Loss Recovery
- Provision replacement infrastructure
- Restore replica data from external snapshots
- Start replicas with original cluster metadata
- Validate quorum, health endpoints, and smoke tests
- Re-enable traffic after validation gates pass
Data Corruption Recovery
- Isolate affected replica(s)
- Preserve forensic artifacts and logs
- Restore from last known-good external snapshot
- 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
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
- Pre-Upgrade Checklist
- Version Compatibility
- Upgrade Procedures
- Health-Based Planning Thresholds
- External Rollback
- Post-Upgrade Verification
- Troubleshooting
- CLI Reference
Overview
ArcherDB upgrades follow a rolling upgrade philosophy designed for zero-downtime deployments:
- One node at a time - Never upgrade multiple replicas simultaneously
- Followers first, primary last - Minimizes disruption to write operations
- Health-based rollback planning - Thresholds for your deployment tooling to decide when to stop or roll back
- 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
Recommended Checks
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 < 100msVersion Compatibility
ArcherDB follows the TigerBeetle model for version compatibility:
Upgrade Rules
- Sequential upgrades: Each version specifies the oldest compatible source version
- Skip versions: May require intermediate upgrades (check CHANGELOG)
- Data format: Backwards compatible within major versions
- 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-runVersion 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: healthyStep 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-amd64Step 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 -fAfter 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 lagStep 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:3000Kubernetes 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 archerdbStep 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:3000Step 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:3000Using 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 timeoutUse 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 minutesExternal 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 toolOr 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 archerdbKubernetes 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 archerdbPost-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_lagPerformance 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_totalFunctional 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_lagSolutions:
- Increase catchup timeout:
--catchup-timeout=600 - Check network connectivity between replicas
- Verify disk I/O is not saturated
- 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 archerdbSolutions:
- Manually stop and restart each replica with old binary
- Check for disk space issues
- Verify network connectivity
Version Incompatibility Error
Symptom: Upgrade reports “Version incompatible - sequential upgrade required”
Solutions:
- Check CHANGELOG for upgrade path requirements
- Perform intermediate upgrade first
- 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 rollbackSolutions:
- Increase thresholds:
--p99-threshold-x10=30 - Review baseline latency (may have been unusually low)
- 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.
Related Documentation
- Operations Runbook - General operational procedures
- Backup Operations - External snapshot and restore procedures
- Disaster Recovery - DR planning and procedures
- Monitoring - Metrics and alerting
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
- Quick Diagnosis
- How to Use This Guide
- Connection Issues
- Performance Issues
- Cluster Issues
- Query Issues
- Replication Issues
- Data Protection Issues
- Diagnostic Commands
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:
- Server not running
- Wrong port number
- Firewall blocking the port
- Binding to wrong network interface
Resolution:
Check if server is running:
systemctl status archerdb # Or pgrep -f archerdbIf not running, start it:
systemctl start archerdbVerify listening port:
ss -tlnp | grep archerdb # Or netstat -tlnp | grep 3000Ensure ArcherDB is listening on the expected port.
Check firewall rules:
# Linux (iptables) iptables -L -n | grep 3000 # Linux (firewalld) firewall-cmd --list-ports # Cloud: Check security groups/firewall rules in consoleVerify 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:
- Network routing issues
- DNS resolution failure
- Load balancer misconfiguration
- Server overloaded
Resolution:
Test network connectivity:
# From client machine ping node1 telnet node1 3000 nc -zv node1 3000Verify DNS resolution:
nslookup node1 dig node1Check load balancer health:
# Verify backend health in LB dashboard # Check LB logs for connection errorsCheck 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:
- Client configured with wrong cluster ID
- Connecting to wrong cluster
- Data file from different cluster
Resolution:
Check server cluster ID:
./archerdb info /data/archerdb.db | grep cluster # Or check startup logs for "cluster_id"Update client configuration:
# Ensure cluster_id matches server client = ArcherDBClient( addresses=["node1:3000"], cluster_id=12345 # Must match server )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:
- Certificate expired
- Hostname mismatch
- CA certificate not trusted
- Protocol/cipher mismatch in gateway or service mesh
Resolution:
Check certificate expiry in gateway/proxy:
openssl x509 -in /path/to/cert.pem -noout -datesIf expired, rotate certificates (see Certificate Rotation).
Verify hostname/SAN matches exposed endpoint:
openssl x509 -in /path/to/cert.pem -noout -text | grep -A1 "Subject Alternative Name"Test TLS endpoint directly:
openssl s_client -connect node1:3000 -CAfile /path/to/ca.pemCheck 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:
- Disk I/O saturation
- Compaction running
- Large batch sizes causing queuing
- Insufficient memory for block cache
Resolution:
Check disk I/O:
iostat -x 1 5 # Look for %util > 80% or high await timesIf disk saturated, consider faster storage (NVMe).
Check for active compaction:
curl -s localhost:9090/metrics | grep archerdb_compaction_active curl -s localhost:9090/metrics | grep archerdb_compaction_write_ampCompaction is normal but can cause latency spikes. See LSM Tuning.
Reduce batch sizes: Large batches (>5000 events) can cause queuing delays. Optimal range: 500-2000.
Check memory pressure:
curl -s localhost:9090/metrics | grep process_resident_memory_bytes free -hIf 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:
- Connection pool too small
- Batch sizes too small
- Client-side bottleneck
- Network bandwidth limit
Resolution:
Increase connection pool:
client = ArcherDBClient( addresses=["node1:3000"], pool_size=10 # Increase from default )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()Profile client application: Ensure client isn’t CPU-bound processing results.
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:
- RAM index grown beyond capacity plan
- Memory leak (rare)
- Large query result sets in memory
Resolution:
Check entity count vs. capacity:
curl -s localhost:9090/metrics | grep archerdb_entities_total # Compare to capacity planSee Capacity Planning for sizing.
Check index load factor:
curl -s localhost:9090/metrics | grep archerdb_index_load_factor # Should be < 0.7 for optimal performanceRestart 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:
- TTL not configured
- Compaction falling behind
- High update rate creating versions
Resolution:
Check if TTL is configured:
./archerdb info /data/archerdb.db | grep ttlConsider enabling TTL for automatic cleanup.
Check compaction status:
curl -s localhost:9090/metrics | grep archerdb_lsm_levels curl -s localhost:9090/metrics | grep archerdb_compactionIf levels accumulating, compaction may be behind.
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:
- Network partition between replicas
- Clock skew too large
- Data corruption preventing startup
- Wrong cluster configuration
Resolution:
Test network connectivity between replicas:
# From each node, test others for node in node1 node2 node3; do echo "Testing $node" nc -zv $node 3000 doneCheck clock synchronization:
chronyc tracking # Or timedatectl status # Clock skew should be < 100msIf skewed, fix NTP:
systemctl restart chronydVerify data file integrity:
./archerdb verify /data/archerdb.dbIf corrupted, see Disaster Recovery.
Check configuration consistency: Ensure all replicas use same
--addresseslist and--clusterID.
Prevention: Monitor clock skew and network connectivity. Use consistent configuration management.
Frequent View Changes
Symptom: archerdb_view_changes_total
incrementing frequently.
Possible Causes:
- Network instability
- Disk I/O latency causing heartbeat timeouts
- Resource exhaustion (CPU/memory)
Resolution:
Check network stability:
# Test packet loss between replicas ping -c 100 node2 | grep "packet loss"Check disk latency:
iostat -x 1 5 # Check await column - should be < 10ms for SSDCheck 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:
- Slow disk on follower
- Network congestion to follower
- Follower under-resourced
Resolution:
Check disk performance on lagging replica:
ssh lagging-node "iostat -x 1 5"Check network path:
iperf3 -c lagging-node -p 5201 mtr lagging-nodeCompare 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:
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" doneAll healthy replicas should have the same view number.
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:
- Coordinate encoding mismatch (degrees vs. nanodegrees)
- Query area actually empty
- Wrong group ID filter
- Data not yet replicated
Resolution:
Verify coordinate encoding:
# ArcherDB uses nanodegrees internally # SDK should handle conversion, but verify: lat = 37.7749 # degrees lat_nano = 37774900000 # nanodegrees (lat * 1e9)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 )Verify group ID:
# Try without group filter results = client.query_radius(..., group_id=None)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:
- Wrong winding order (exterior must be counter-clockwise)
- Self-intersecting polygon
- Too few vertices (minimum 4 for closed ring)
- Holes with wrong winding order (must be clockwise)
Resolution:
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 ]Check for self-intersection: Use a GIS tool or library to validate the polygon.
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:
- Result limit reached (default: 1000)
- Pagination required
- Filter excluding data (group_id, time range)
Resolution:
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)}")Increase limit if needed:
results = client.query_radius(..., limit=10000) # Max: 10000Remove 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:
- Invalid credentials
- Bucket permissions
- Network connectivity to S3
- S3 service outage
Resolution:
Check credentials:
# Verify AWS credentials aws sts get-caller-identity # Test S3 access aws s3 ls s3://your-bucket/Check bucket policy: Ensure IAM role/user has
s3:PutObject,s3:GetObject,s3:ListBucket.Test network connectivity:
curl -I https://s3.amazonaws.com # Or your regional endpointCheck 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:
- S3 throttling
- Network bandwidth limitation
- High write volume
Resolution:
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-bucketCheck upload bandwidth:
curl -s localhost:9090/metrics | grep archerdb_replication_bytesConsider 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:
- S3 outage or prolonged failure
- Credentials expired
- Network partition to S3
Resolution:
Check S3 connectivity:
aws s3 ls s3://your-bucket/Check credential expiry: For IAM roles, ensure instance profile is attached.
Monitor spillover directory:
du -sh /data/spillover/ ls -lt /data/spillover/ | headWhen 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:
- New volume created outside policy
- Snapshot replication policy disabled
- Wrong storage class or account defaults
Resolution:
- Verify encryption settings on active data volumes and snapshots.
- Recreate non-compliant resources with enforced encryption policy.
- Re-run restore drill from compliant snapshot set.
External Key Service Issues
Symptom: Platform tooling reports KMS/key policy failures.
Possible Causes:
- KMS connectivity or endpoint policy issue
- IAM permissions drift
- Key disabled/deleted
Resolution:
- Validate key accessibility with platform tooling.
- Restore least-privilege IAM/key policies from IaC baseline.
- 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, replicationMetrics 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_totalLog 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 /dataCluster 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_numberGetting 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
doneWhat to Include in Bug Reports
When opening a GitHub issue, include:
- Environment: OS, ArcherDB version, hardware specs (CPU, RAM, disk type)
- Configuration: Cluster size, relevant config settings
- Reproduction steps: Exact sequence to reproduce the issue
- Expected behavior: What should happen
- Actual behavior: What actually happens
- Logs: Relevant log snippets (redact sensitive data)
- 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:
- Data loss suspected: Stop writes, capture external snapshot, preserve logs
- Cluster unavailable: Check quorum (need 2/3 or 3/5 replicas)
- Security incident: Isolate affected nodes, preserve evidence, rotate credentials
See Disaster Recovery for emergency procedures.
Related Documentation
- Operations Runbook - Operational procedures
- Disaster Recovery - Recovery procedures
- Error Codes Reference - Complete error code list
- Capacity Planning - Sizing guidance
- LSM Tuning - Storage performance tuning
- Performance Tuning - Optimization guide
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 errorJava
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 |
Related Documentation
- SDK Retry Semantics - Detailed retry configuration
- Disaster Recovery - Recovery procedures
- Operations Runbook - Operational procedures
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
- [ ] Check if the pod/process is running
- [ ] Verify network connectivity to the replica
- [ ] Check for resource exhaustion (OOM, disk full)
- [ ] 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 3000Resolution
Process Crashed
Check logs for crash reason:
kubectl logs archerdb-N -n archerdb --previousIf OOM killed, increase memory limits:
resources: limits: memory: "8Gi" # Increase from defaultRestart the pod:
kubectl delete pod archerdb-N -n archerdb # StatefulSet will recreate it
Node Failure
Check node status:
kubectl get nodes kubectl describe node <node-name>If node is unhealthy, pod will be rescheduled automatically (may take 5+ minutes).
For faster recovery, delete the pod to trigger immediate reschedule:
kubectl delete pod archerdb-N -n archerdb --force --grace-period=0
Network Partition
Verify network policies allow inter-pod communication:
kubectl get networkpolicy -n archerdbCheck DNS resolution:
kubectl exec archerdb-0 -n archerdb -- nslookup archerdb-N.archerdb-headless.archerdb.svc.cluster.localTest port connectivity:
kubectl exec archerdb-0 -n archerdb -- nc -zv archerdb-N.archerdb-headless.archerdb.svc.cluster.local 3000
Resource Exhaustion
- Out of memory: Increase memory limits or reduce entity count
- Disk full: See Disk Capacity Runbook
- File descriptors: Check ulimits and increase if needed
Prevention
- PodDisruptionBudget: Configure
minAvailable: 2to 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/detailedRelated Documentation
- Operations Runbook - Cluster management procedures
- Disaster Recovery - Recovery from total failure
- Troubleshooting Guide - General troubleshooting
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
- [ ] Check all replica health status
- [ ] Identify which replica(s) are triggering view changes
- [ ] Check for network issues between replicas
- [ ] 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 5Log 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
Identify network issues:
# Test sustained connectivity kubectl exec archerdb-0 -n archerdb -- mtr -c 100 --report archerdb-1.archerdb-headless.archerdb.svc.cluster.localCheck for network policy issues:
kubectl get networkpolicy -n archerdb -o yamlFor cloud deployments: Check cloud provider status and network logs.
CPU/Disk Saturation
Check resource usage:
kubectl top pod -n archerdb kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5If CPU saturated: Increase CPU limits or investigate high-CPU operations.
If disk slow: Check for compaction backlog or storage issues. See Compaction Backlog.
Clock Skew
Check time sync status:
kubectl exec archerdb-0 -n archerdb -- chronyc tracking # Or kubectl exec archerdb-0 -n archerdb -- timedatectl statusIf clock skewed > 100ms: Fix NTP configuration on affected nodes.
Failing Replica
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 doneCheck that replica’s logs:
kubectl logs archerdb-N -n archerdb --since=30m | grep -E "(error|warning|panic)" -iRestart 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
Related Documentation
- Operations Runbook - Cluster management
- Troubleshooting Guide - Detailed view change troubleshooting
- Replica Down - If view changes lead to replica failure
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
- [ ] Check current entity count vs index capacity
- [ ] Assess query latency impact
- [ ] Plan capacity increase (requires restart)
- [ ] Schedule maintenance window if immediate action needed
Investigation
Common Causes
- Entity growth: Data volume exceeded capacity planning assumptions
- Under-provisioned: Initial
ram_index_capacitywas 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 indexImpact 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.
Calculate required capacity:
Current entities: N Target capacity: N * 2 (for 50% load factor) Recommended minimum: 500,000 (Phase 5 optimization)Update configuration:
For Helm deployment:
# values.yaml config: ram_index_capacity: 1000000 # Increase to 1MFor bare metal:
# Update startup script or systemd unit ./archerdb start --ram-index-capacity=1000000 ...Perform rolling restart:
# Kubernetes - update StatefulSet kubectl rollout restart statefulset/archerdb -n archerdb # Monitor rollout kubectl rollout status statefulset/archerdb -n archerdbFor 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_secondsRelated Documentation
- Capacity Planning - Sizing guidelines
- LSM Tuning - Storage performance tuning
- High Read Latency - If degraded index causes latency alerts
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
- [ ] Check for active compaction
- [ ] Verify disk I/O is not saturated
- [ ] Check for index degradation (probe limit hits)
- [ ] 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
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 compactionCompaction 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 threadsSee LSM Tuning for detailed compaction tuning.
Disk Saturation
Check disk utilization:
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5 # %util > 80% indicates saturationResolution options:
- Reduce write rate if possible
- Upgrade to faster storage (NVMe recommended)
- Increase compaction threads to complete faster
Index Degradation
Check for probe limit hits:
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_index_probe_limit_hits_totalIf counter is increasing, see Index Degraded Runbook.
Large Result Sets
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
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
Check cache effectiveness:
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_cache_hit_ratioIf 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\"'Related Documentation
- LSM Tuning - Compaction and storage tuning
- Index Degraded - RAM index issues
- Capacity Planning - Sizing guidelines
- Troubleshooting Guide - General latency troubleshooting
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
- [ ] Check compaction backlog size
- [ ] Verify disk I/O is not saturated
- [ ] Check WAL directory usage
- [ ] 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_lagLog 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
Check backlog size:
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_compaction_pending_bytesIf > 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 compactionFor immediate relief, reduce write rate temporarily:
- Increase batch submission interval
- Defer non-critical writes
See Compaction Backlog Runbook for detailed guidance.
Disk Saturation
Check disk metrics:
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5If saturated:
- Upgrade to faster storage (NVMe)
- Reduce write rate
- Consider sharding to distribute writes
Large Batch Sizes
Check batch size metrics:
kubectl exec archerdb-0 -n archerdb -- curl -s localhost:9090/metrics | grep archerdb_batch_sizeIf batches > 5000 events:
- Reduce batch size to 1000-2000 for lower latency
- Trade-off: smaller batches = lower throughput but more consistent latency
Consensus Delays
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 doneIf lag > 100ms, investigate network:
kubectl exec archerdb-0 -n archerdb -- ping -c 10 archerdb-1.archerdb-headless.archerdb.svc.cluster.localSee 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 overheadPrevention
- 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\"'Related Documentation
- Compaction Backlog - Compaction-specific guidance
- LSM Tuning - Storage performance tuning
- Capacity Planning - Sizing guidelines
- Troubleshooting Guide - General latency troubleshooting
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_bytesarcherdb_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
- [ ] Check current disk usage and free space
- [ ] Identify largest consumers of space
- [ ] Check if TTL cleanup is configured and running
- [ ] 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_compactionCommon 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
Check for non-essential files:
kubectl exec archerdb-0 -n archerdb -- ls -la /data/ # Look for spillover/, tmp/, or snapshot export filesCheck spillover directory:
kubectl exec archerdb-0 -n archerdb -- du -sh /data/spillover/ 2>/dev/null # If large, check S3 log-shipping statusForce 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
Check current TTL settings:
kubectl exec archerdb-0 -n archerdb -- ./archerdb info /data/archerdb.db | grep -i ttlEnable TTL via configuration:
# values.yaml config: ttl_enabled: true ttl_default_hours: 168 # 7 days defaultTTL cleanup runs automatically and removes expired events during queries and compaction.
Expand Storage Capacity
For Kubernetes PVC:
Check if StorageClass allows expansion:
kubectl get storageclass -o jsonpath='{.items[*].allowVolumeExpansion}'Expand PVC:
kubectl patch pvc data-archerdb-0 -n archerdb -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'Note: Pod restart may be required for some storage classes.
For bare metal:
- Expand underlying storage (LVM, cloud disk, etc.)
- Resize filesystem:
resize2fs /dev/sdX
Archive Old Data
If immediate deletion is not acceptable:
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).tarVerify archive success:
aws s3 ls s3://archive-bucket/ | tail -n 5Consider 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
- Set appropriate TTL for your use case
- Use time-partitioned groups for easier archival
- Implement data lifecycle policies
Emergency Procedures
If Disk is 100% Full
Database may be read-only. Immediate action required.
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/nullIf database is read-only, restart after freeing space:
kubectl delete pod archerdb-0 -n archerdbExpand 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.dbVerification
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'Related Documentation
- Capacity Planning - Sizing guidelines
- Backup Operations - External snapshot procedures
- Compaction Backlog - If compaction is contributing to space issues
- Operations Runbook - General operations
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
- [ ] Check current L0 file count
- [ ] Verify compaction is running
- [ ] Check disk I/O capacity
- [ ] 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 5Log 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 onlyIncrease compaction threads (if CPU available):
lsm_compaction_threads: 4 # Increase from 3Adjust L0 trigger (trade-off: higher = more read amplification):
lsm_l0_compaction_trigger: 12 # Allow more L0 buildupApply changes with rolling restart:
kubectl rollout restart statefulset/archerdb -n archerdb
Reduce Write Rate
If compaction cannot keep up even with tuning:
- Temporary relief: Increase batch submission interval
- Defer non-critical writes during peak hours
- Consider rate limiting at application layer
Upgrade Storage
If disk I/O is the bottleneck:
Check current I/O utilization:
kubectl exec archerdb-0 -n archerdb -- iostat -x 1 5 # %util > 80% indicates saturationUpgrade 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:
- Consider adding shards to distribute write load
- Each shard handles a subset of data
- 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_ampPrevention
- 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_secondsRelated Documentation
- LSM Tuning - Detailed compaction tuning guide
- High Write Latency - If backlog causes latency alerts
- Disk Capacity - If backlog affects disk usage
- Capacity Planning - Sizing guidelines
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 insrc/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:
0—PASS: kernel-crash durability — data file verified after hard reset1—FAIL: 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-afterseconds (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-mband 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=nativedoes not model. For that, combine this harness withdm-flakey-based injection on the host —scripts/dm_flakey_test.shexists 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 optionalavailable_capacityfor 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.
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:
- 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.
- 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:
- The Python benchmark harness under
test_infrastructure/benchmarks/ - GitHub Actions workflows for baseline comparison and published
history under
.github/workflows/benchmark.ymland.github/workflows/benchmark-weekly.yml - The correctness-gated external comparison suite under
scripts/competitor-benchmarks/v2/
Local Outputs
Local benchmark runs write to:
reports/benchmarks/for detailed run outputsreports/history/for local historyreports/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.jsonbenchmarks/history/2026-08-03-comparison-v2.jsonbenchmarks/comparison-v2/report.mdbenchmarks/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-mixedThe 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
5voters plus1standby - passes
--replica-countthrough bothformatandstart - 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.jsonreports/benchmarks/release-20260409-sdkquick/20260409-055217-3node.jsonreports/benchmarks/release-20260409-sdkquick/20260409-063550-5node.jsonreports/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 pageArcherDB 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=liteQuick 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 10000Full 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-mixedMixed 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.0Compare to Baseline
# Compare current run to stored baseline
python3 test_infrastructure/benchmarks/cli.py compare baseline.json current.jsonInterpreting 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:
- Spins up clusters (1/3/5/6 nodes)
- Runs full benchmark suite
- Compares to baseline
- Alerts on >10% regression
- Can promote approved results into
benchmarks/history/ - 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.ymlProgrammatic 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
- Single-Node Evidence, August 2026 - Measured ArcherDB vs Valkey vs PostGIS comparison with reproduction steps
- Detailed Benchmark Framework - Statistical methodology
- Testing Guide - Running tests locally
- CI Tiers - Weekly benchmark workflow
- Performance Tuning - Optimization guidance
Last updated: 2026-02-01
Edit this pageSingle-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 useappendonly yes+appendfsync always. - PostGIS 16-3.4: official docker image, schema + GIST index from
scripts/competitor-benchmarks/setup-postgis.sh,psycopg2.execute_valuesbatched 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 --jsonCaveats: 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).
ArcherDB Profiling Guide
This guide covers CPU profiling and performance analysis for ArcherDB using Linux perf and flame graphs.
Table of Contents
- Prerequisites
- Quick Start
- Flame Graphs
- Hardware Counter Profiling
- Profiling Workflows
- A/B Benchmarking with POOP
- Memory Profiling
- Troubleshooting
- Best Practices
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 perfInstall 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/FlameGraphConfigure 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.confFrame 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.svgCollect Hardware Counters
# Profile hardware counters (5 runs for statistics)
./scripts/profile.sh -- ./zig-out/bin/archerdb benchmarkFlame 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 benchmarkOptions
-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:
- S2 Cell Operations: Functions in
s2/directory handling spatial indexing - LSM Operations: Compaction, level management in the storage layer
- Network I/O: Message serialization/deserialization
- Memory Allocation: Frequent allocator calls may indicate optimization opportunities
Hot spots to watch:
s2.region_coverer.getCovering- Spatial query coverageram_indexoperations - In-memory indexingio_uringsubmission 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 benchmarkOptions
-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 100000Profiling 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:3001A/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 sideContinuous 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/poopBasic 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:
- Verify build: Ensure you built with
./zig/zig build(frame pointers are enabled by default) - Check binary: Run
readelf -S zig-out/bin/archerdb | grep -i frameto verify - Kernel symbols: For kernel stacks, ensure
/proc/kallsymsis 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=1Alternatively, run with sudo:
sudo ./scripts/flamegraph.sh --output profile.svg -- ./zig-out/bin/archerdb benchmark“No samples collected” or Empty Flame Graph
- Duration too short: Increase duration with
-d 60 - Workload too fast: The command finished before sampling started
- 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 benchmarkperf 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 benchmarkFlame Graph SVG Won’t Open
- File too large: Reduce sampling duration or increase frequency
- Corrupted output: Check for error messages during generation
- Browser issues: Try a different browser (Chrome/Firefox work best)
Best Practices
When to Profile
- Before optimization: Establish baseline
- After optimization: Verify improvement
- Before release: Ensure no regressions
- After refactoring: Confirm performance preserved
Profiling Checklist
Common Pitfalls
- Debug builds: 10-100x slower, misleading profiles
- Small samples: High variance, unreliable results
- Cold caches: First run different from steady state
- Compiler optimizations: Release builds may inline/eliminate code
- System noise: Background processes affect measurements
Optimization Priority
Focus optimization efforts based on profiling data:
- Hot paths: Functions consuming >10% of CPU time
- Cache misses: High miss rate (>10%) in critical paths
- Branch mispredictions: High miss rate (>5%) in loops
- 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=trueUsing 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 operationsRunning Tracy
- Download Tracy profiler from https://github.com/wolfpld/tracy/releases
- Run the ArcherDB binary built with
-Dtracy=true - Connect Tracy profiler to the running process
- 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 statusParca 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.yamlFor production, consider Parca Cloud or self-hosted deployment.
Analyzing Profiles
- Open Parca UI at http://localhost:7070
- Select time range and process
- View flame graph of CPU usage over time
- 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=trueThe profile build:
- Uses
ReleaseFastoptimization for representative performance - Preserves frame pointers for accurate stack traces
- Outputs
archerdb-profilebinary
Additional Resources
- Brendan Gregg’s Flame Graphs - Original flame graph documentation
- perf Examples - Comprehensive perf tutorial
- Linux Perf Wiki - Official perf documentation
- Tracy Profiler - Real-time frame profiler
- Parca Documentation - Continuous profiling
Related Documentation
- Benchmarks - Performance benchmark results
- Hardware Requirements - System recommendations
- Architecture - System design overview
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_stallsmetric 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 throughputRead-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 cacheMixed 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: 1024Benchmarking
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-suiteThe 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_compactionIf compaction_pending_bytes is
high:
- Increase
compaction_threadsto 3-4 - Increase
l0_compaction_triggerto 8-12 - 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_sizeors2_covering_cache_size
Symptom: IndexDegraded Alert
Diagnosis:
curl -s localhost:9090/metrics | grep archerdb_index_load_factorIf load factor > 0.7:
- Increase
ram_index_capacity(requires restart) - Or scale horizontally (add shards)
- Or reduce entity count (TTL, archival)
Related Documentation
- LSM Tuning Guide - Deep dive on LSM configuration
- Capacity Planning - Sizing your deployment
- Architecture - Understanding ArcherDB internals
- Troubleshooting - General troubleshooting guide
.planning/phases/05-performance-optimization/05-VERIFICATION.md- Detailed benchmark results
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
- RAM index pressure (
- 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=100Maintained benchmark harness
python3 test_infrastructure/benchmarks/cli.py run --topology 3 --time-limit 60Capacity test (real run)
python3 scripts/test_capacity_limits.py --config lite --optimize ReleaseFast
python3 scripts/test_capacity_limits.py --config standard --optimize ReleaseFastBy default, capacity test artifacts are written to
/tmp/archerdb_capacity_runs.
Interpreting Results
For capacity runs, verify:
- No early transport bottleneck (
status=1) at normal batch sizes. - Throughput remains in the same order of magnitude across tiers on the same hardware.
- Failure reason changes with capacity quotas, not tier runtime behavior.
Example summary fields:
events_insertedunique_entriescpu_percent_avg/cpu_percent_peakram_rss_avg_bytes/ram_rss_peak_bytesdisk_logical_bytes/disk_physical_bytes_from_dufailure_reasonandfirst_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-sizecan lower RAM index budget at runtime but cannot exceed the tier cap.- Increase
ram_index_size_defaultonly if product intent requires a higher capacity boundary.
Capacity run fails on storage limit
- Verify
storage_size_limit_defaultandstorage_size_limit_maxfor the selected tier. - Use larger tier quotas when the workload requires longer retention.
References
src/config.zigsrc/constants.zigscripts/test_capacity_limits.pyzig-out/bin/archerdb benchmarktest_infrastructure/benchmarks/cli.py
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 build2. 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 30013. 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/ -vNode.js:
cd src/clients/node
npm install
npm testGo:
cd src/clients/go
go test ./... -vJava:
cd src/clients/java
mvn testC:
cd src/clients/c
make testServer (Zig unit tests):
./zig/zig build -j4 -Dconfig=lite test:unit4. 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 integrationParity 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 -vResults are written to:
reports/parity.json- Machine-readabledocs/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
Check if binary exists:
ls zig-out/bin/archerdbBuild if missing:
./zig/zig build -j4 -Dconfig=liteCheck for port conflicts:
lsof -i :3001
Tests Fail with Connection Errors
Verify server is running:
curl http://127.0.0.1:3001/ping # Should return: {"pong":true}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:unitPreserving 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 checkCI 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
- CI Tier Structure - CI pipeline organization
- Performance Baselines - Regression thresholds and benchmark baselines
- Benchmark Guide - Performance testing
- SDK Comparison Matrix - SDK feature parity
- SDK Limitations - Known issues and workarounds
- Parity Matrix - Cross-SDK verification status
Last updated: 2026-02-01
Edit this pageCI 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=60PR:
./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300Nightly (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 stopWeekly (requires cluster setup):
python3 test_infrastructure/benchmarks/cli.py run --full-suiteWorkflow 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
- Testing Guide - Local test running
- Benchmark Guide - Performance testing details
- Parity Matrix - SDK verification status
Last updated: 2026-02-01
Edit this pagePerformance 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
- Main branch pushes upload new baseline artifact (full benchmark mode)
- PRs download the current main baseline and run quick benchmarks
- Comparison checks both throughput and P99 latency against thresholds
- 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
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}
Merge your PR (no comparison runs without baseline)
New baseline created on next main push
Alternative: Update Baseline Manually
If you don’t want to delete the artifact:
- Merge to main (workflow runs)
- New baseline automatically uploaded
- 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:
- Review recent commits for performance-impacting changes
- Profile locally to identify hot paths
- Fix the performance issue
- 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 bcConfiguration
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:
- Edit the multipliers in
benchmark-ci.sh - Update this documentation
- Update workflow header comments
Benchmark Modes
| Mode | Duration | Use Case |
|---|---|---|
| quick | ~30 seconds | PRs (fast feedback) |
| full | ~5 minutes | Main branch (accurate baseline) |
Related Files
.github/workflows/benchmark.yml- CI workflowscripts/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
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=60PR:
./scripts/test-constrained.sh unit
pytest tests/ -v --timeout=300Nightly (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 stopWeekly (requires cluster setup):
python3 test_infrastructure/benchmarks/cli.py run --full-suiteWorkflow 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
- Testing Guide - Local test running
- Benchmark Guide - Performance testing details
- Parity Matrix - SDK verification status
Last updated: 2026-02-01
Edit this pageArcherDB 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/archerdbQuick 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/1001Expected 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-4466554400005. 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/pingExpected response (healthy):
{"pong":true}10. Status
Get server status and statistics.
curl http://localhost:3001/statusExpected 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/topologyExpected 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
- API Reference - Detailed operation documentation
- Protocol Reference - Wire format details
- Error Codes - Complete error reference
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,000lon_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 leader222- Resharding in progress- Network timeouts
Non-retryable errors (fix request first):
7- Entity ID must not be zero9- Latitude out of range10- Longitude out of range100-199- Validation errors300- 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
- API Reference - Operation details and SDK examples
- Error Codes - Complete error reference
- curl Examples - Working curl examples for all operations
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:
- VSR Consensus Protocol - How data stays consistent
- LSM-Tree Storage - How data is persisted
- S2 Geospatial Indexing - How spatial queries work
- RAM Index - How “where is entity X?” queries are fast
Table of Contents
- Introduction
- System Overview
- Viewstamped Replication (VSR)
- LSM-Tree Storage
- S2 Geospatial Indexing
- RAM Index
- Sharding
- Replication
- 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:
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.
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.
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”):
- SDK serializes the request and sends to the primary replica
- VSR assigns a timestamp and broadcasts to all replicas (Prepare phase)
- Backups acknowledge receipt (Prepare OK)
- Primary commits after quorum acknowledgment
- State Machine executes the operation deterministically
- LSM-Tree persists the GeoEvent durably
- RAM Index updates the latest position cache
- S2 Index enables future spatial queries
- 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:
No Log Truncation: Unlike Raft, VSR never truncates committed entries. This simplifies crash recovery and eliminates a class of subtle bugs.
Deterministic Replay: The same sequence of operations produces identical state on all replicas, enabling VOPR (Viewstamped Operation Prover) simulation testing.
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:
Op Ordering: Operation numbers strictly increase, and the hash chain enforces that all replicas process operations in the same order.
Commit Safety: An operation is only committed after quorum acknowledgment, preventing data loss on primary failure.
View Monotonicity: View numbers only increase, preventing split-brain scenarios where two replicas both believe they are primary.
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
- Memtable: Writes first go to an in-memory sorted structure (memtable)
- Flush: When the memtable fills, it’s flushed to Level 0 as an immutable SSTable
- Level 0: Contains recent SSTables with potentially overlapping key ranges
- 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:
- Check Memtable: If key is in memory, return immediately (fastest)
- Check Level 0: Scan all L0 files (they may overlap)
- 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”
- Compute covering: Generate S2 cells that cover the 1km circle
- Scan cells: For each covering cell, query the LSM tree for events in that cell range
- 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”
- Compute covering: Generate S2 cells that cover the polygon
- Scan cells: Query LSM for events in covering cells
- 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:
- Checking the memtable (fast)
- Potentially checking multiple L0 files (slower)
- 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:
- Heap Mode: Faster, but lost on restart. Rebuilt by scanning the LSM tree.
- 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:
- Coordinator receives query
- Fan out to all relevant shards in parallel
- Aggregate results
- 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
- Primary commits via VSR (synchronous within region)
- WAL entries shipped to S3 bucket (asynchronous)
- Follower pulls from S3 and applies entries
- 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:
- Memory over Disk: RAM index uses ~91GB for 1B entities - we optimize for speed, not memory efficiency
- Writes over Reads: LSM trees have read amplification - acceptable because location tracking is write-heavy
- Consistency over Availability: VSR requires quorum - we choose correctness over availability during partitions
- Simplicity over Flexibility: Single-purpose design - not a general-purpose database
Further Reading
- VSR Deep Dive - Consensus protocol internals
- LSM Tuning Guide - Storage layer configuration
- Performance Tuning - Optimizing for your workload
- API Reference - Operations and endpoints
- Durability Verification - How we test correctness
- Operations Runbook - Running ArcherDB in production
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
- START_VIEW_CHANGE (SVC): Broadcast to all replicas, wait for quorum
- DO_VIEW_CHANGE (DVC): New primary collects from quorum
- START_VIEW (SV): New primary broadcasts to confirm new view
Quorums
quorum_replication: Majority required for prepare ackquorum_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_opsoperations (configurable) - After compaction completes
- Before state sync to peer
Recovery from Checkpoint
- Read superblock, verify integrity
- Load state machine from checkpoint_id
- Replay journal from checkpoint_op + 1
- 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
- Op ordering:
opstrictly increases, hash chain enforces order - Commit safety: Only commit after
quorum_replicationacks - View monotonicity:
view >= log_view >= view_durable - Checkpoint bounds: Checkpoints every
vsr_checkpoint_ops - 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
- TigerBeetle VSR Documentation: https://github.com/tigerbeetle/tigerbeetle
- Viewstamped Replication Revisited: https://pmg.csail.mit.edu/papers/vr-revisited.pdf
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
Consensus Protocol (VSR)
- View changes when primary fails
- Prepare/commit message handling
- Quorum formation and maintenance
- Replica synchronization
WAL (Write-Ahead Log)
- Crash during prepare phase
- Crash during commit phase
- Partial/torn writes
- Journal recovery after crash
Checkpoints
- Crash during checkpoint write
- Superblock integrity
- State recovery from checkpoint
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=200Extended verification (1-8 hours):
./scripts/run_vopr.sh --seeds "$(seq 1 100)" --requests-max=10000 --no-liteWith aggressive crash injection:
./scripts/run_vopr.sh --crash-rate=1 --requests-max=1000Testing specific cluster configurations:
# 3-node cluster (default)
./scripts/run_vopr.sh --replicas=3
# 5-node cluster
./scripts/run_vopr.sh --replicas=5Debugging 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
- Process crash recovery: ArcherDB must recover correctly after SIGKILL
- Deterministic behavior: Same seed must produce same results after restart
- 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=500How It Works
- Start VOPR with a known seed
- Wait a random time (1 to
--timeoutseconds) - Send SIGKILL to the process
- Restart VOPR with the same seed
- 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
- Power loss during write: Data written but not synced
- Partial writes: Only part of a sector written
- Drop writes: Writes acknowledged but not persisted
- 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=500How It Works
- Create a loop device backed by a file
- Create a dm-flakey device on top of the loop device
- Format and mount the dm-flakey device
- Run a real
archerdb benchmarkworkload against a data file on that mount - Trigger
drop_writesduring the live workload - Stop the workload process group after the fault window
- Restore disk access and remount the filesystem
- Run
archerdb verifyon the recovered file - Restart
archerdb startand 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:
- Full disk: Currently not simulated
- Kernel crash: Requires VM-based testing
- Hardware memory corruption: ECC testing requires special hardware
- Byzantine failures: VSR assumes crash-fail model, not Byzantine
- 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=200This 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=100000Release Verification
Before each release:
- 24-hour VOPR run with multiple seeds
- All cluster configurations (3, 5, 6 replicas)
- SIGKILL testing on Linux and macOS
- dm-flakey testing on Linux
Reproducing Failures
From CI Failure
- Note the seed from CI output (commit hash or explicit seed)
- Run locally with the same seed:
./scripts/run_vopr.sh --replay <seed>
From Production Issue
- Collect the data directory
- Note the replica count and configuration
- Run VOPR with similar parameters
- Use
--dump-on-failto capture decision history
Extending Coverage
Adding New Fault Types
- Add fault probability to
src/testing/storage.zig:Options - Implement fault injection in appropriate
step()functions - Add CLI flag in
src/vopr.zig - Update
scripts/run_vopr.sh - Document in this file
Adding New Test Scenarios
- Identify the scenario
- Determine which tool(s) can test it
- Implement (extend VOPR or create new script)
- Add to CI pipeline
- Document coverage
References
- VOPR Source: Main VOPR implementation
- Storage Simulation: Fault injection
- Cluster Simulation: Multi-replica testing
- VSR Protocol: Consensus implementation
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 DROPPort 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:
- Deploy in private subnet: No public IP assignment
- Use security groups: Allow only internal CIDR ranges
- Network ACLs: Block all inbound from 0.0.0.0/0 to database ports
- 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:3001For development environments:
# Forward all ArcherDB ports
ssh -L 3000:localhost:3000 -L 3001:localhost:3001 -L 3002:localhost:3002 user@dev-serverDisk 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/archerdbmacOS (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 archerdbBackup 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.ageFor 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/sdbFor 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:
SSH key-only authentication: Disable password authentication
# /etc/ssh/sshd_config PasswordAuthentication no PubkeyAuthentication yesUse jump boxes/bastion hosts: No direct SSH to database servers
# ~/.ssh/config Host archerdb-* ProxyJump bastion.internal User archerdb-adminPrinciple 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 -fKey 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/advisoriesBackup 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 procedureSecurity 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
Related Documentation
- Disaster Recovery - External backup and restore procedures
- Operations Runbook - Day-to-day operational procedures
- Encryption Guide - External encryption-at-rest controls
- Encryption Security - Data-protection model and non-goals
- Upgrade Guide - Secure upgrade procedures
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.
Recommended Controls
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.
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
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:
- Protocol violations are always fatal (security boundary)
- Peer-initiated disconnects are normal (not errors)
- Resource exhaustion rejects new work, keeps existing connections
- 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:
- Prefer dropping client connections over unknown connections
- Prefer dropping unknown connections over replica connections
- Log at WARN level with evicted peer type
- Future: emit metric for alerting
- 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:
- Terminating during in-progress connect operation
- Peer closed connection before we initiated shutdown
- 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 reuseaccepting: Reserved for in-progress accept operation (inbound)connecting: Outbound connection in progress (to replica)connected: Fully established, may recv/sendterminating: 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:
terminatingstate 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 backofftcp_keepidle,tcp_keepintvl,tcp_keepcnt: TCP keepalivetcp_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"}
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-identicalsrc/vsr/replica.zigblob at ArcherDB’s import commitf77fa3cf. - 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)
- 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. - Tombstone semantics on the lookup path (PR #3735
chain:
81f7e49d0,8a8d77bb3,8116232a7, …) — explicit tombstone variants + fuzzing. - 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).
- Linux IO event-loop refactor (PR #3619,
a6ec07462) — timeout embedded inio_uring_enter,next_tickas top-level; upstream measured ~8% on their standard benchmark plus tail-latency gains. Large; our io/linux.zig has local modifications to merge through. - 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) |