Aerospike is built for sub-millisecond reads and writes at massive scale, but that speed only delivers value when every node in your cluster is healthy. Namespace storage approaching capacity, cluster migrations in progress after a node failure, client connection pool exhaustion, and replication factor violations can all degrade your application silently while Aerospike's internal metrics remain within bounds. Vigilmon adds an independent external layer: HTTP health checks on your Aerospike-connected services that probe real cluster connectivity, surface namespace pressure, and alert on application-level failures before users notice.
This tutorial shows you how to build meaningful monitoring for Aerospike-backed applications using Vigilmon.
What You'll Build
- A health endpoint that probes real Aerospike node connectivity and namespace health
- Namespace storage and eviction pressure detection
- A Vigilmon HTTP monitor with appropriate timeout and assertion settings
- A heartbeat monitor for Aerospike-backed background workers
- A cluster-aware alerting strategy
Prerequisites
- A running Aerospike cluster (single-node dev or multi-node production)
- An application connected to Aerospike (any runtime)
- A free account at vigilmon.online
Step 1: Add a Health Endpoint to Your Aerospike Service
Add a /health endpoint that performs a lightweight Aerospike operation to verify real connectivity and check namespace storage pressure.
Node.js (aerospike — official client)
// routes/health.js
import Aerospike from 'aerospike';
const client = Aerospike.client({
hosts: process.env.AEROSPIKE_HOSTS || 'localhost:3000',
log: { level: Aerospike.log.OFF },
policies: {
timeout: 5000,
},
});
await client.connect();
export async function healthHandler(req, res) {
const checks = {};
let ok = true;
const start = Date.now();
// Check 1: Basic connectivity — write and read a sentinel key
try {
const key = new Aerospike.Key('test', 'health', 'vigilmon-probe');
await client.put(key, { ts: Date.now() });
const record = await client.get(key);
checks.connectivity = 'ok';
checks.connectivityLatencyMs = Date.now() - start;
checks.probeValue = record.bins.ts;
} catch (err) {
checks.connectivity = `error: ${err.message}`;
ok = false;
}
// Check 2: Cluster info — node count and migrations
try {
const info = await client.infoAll('statistics');
const nodes = Object.keys(info);
checks.nodeCount = nodes.length;
let migrationsInProgress = false;
for (const node of nodes) {
const stats = info[node];
const migrating = stats.info?.match(/migrate_progress_send=(\d+)/)?.[1];
if (migrating && parseInt(migrating) > 0) {
migrationsInProgress = true;
}
}
checks.migrationsInProgress = migrationsInProgress;
if (migrationsInProgress) {
checks.clusterStatus = 'migrating — cluster rebalancing';
// Degraded but not down during expected rebalancing
} else {
checks.clusterStatus = 'stable';
}
} catch (err) {
checks.clusterStatus = `error: ${err.message}`;
ok = false;
}
// Check 3: Namespace storage pressure
const namespace = process.env.AEROSPIKE_NAMESPACE || 'test';
try {
const nsInfo = await client.infoAll(`namespace/${namespace}`);
const firstNode = Object.values(nsInfo)[0]?.info || '';
const usedBytesMatch = firstNode.match(/device_used_bytes=(\d+)/);
const totalBytesMatch = firstNode.match(/device_total_bytes=(\d+)/);
const stopWritesMatch = firstNode.match(/stop_writes=(\w+)/);
if (usedBytesMatch && totalBytesMatch) {
const used = parseInt(usedBytesMatch[1]);
const total = parseInt(totalBytesMatch[1]);
const pct = total > 0 ? Math.round((used / total) * 100) : 0;
checks.namespaceStoragePct = pct;
const maxStoragePct = parseInt(process.env.MAX_STORAGE_PCT || '80', 10);
if (pct > maxStoragePct) {
checks.namespaceHealth = `storage high: ${pct}%`;
ok = false;
} else {
checks.namespaceHealth = 'ok';
}
}
if (stopWritesMatch?.[1] === 'true') {
checks.namespaceHealth = 'stop-writes active — namespace full';
ok = false;
}
} catch (err) {
checks.namespaceHealth = `error: ${err.message}`;
ok = false;
}
res.status(ok ? 200 : 503).json({
status: ok ? 'ok' : 'degraded',
service: 'aerospike-service',
checks,
});
}
Python (aerospike — official client)
# routers/health.py
import os, time, aerospike
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter()
config = {
'hosts': [(h.split(':')[0], int(h.split(':')[1]) if ':' in h else 3000)
for h in os.environ.get('AEROSPIKE_HOSTS', 'localhost:3000').split(',')],
'timeout': 5000,
}
aero_client = aerospike.client(config).connect()
@router.get("/health")
async def health():
checks = {}
ok = True
start = time.monotonic()
# Check 1: Write/read probe
try:
key = ('test', 'health', 'vigilmon-probe')
aero_client.put(key, {'ts': int(time.time() * 1000)})
_, _, record = aero_client.get(key)
checks['connectivity'] = 'ok'
checks['connectivityLatencyMs'] = int((time.monotonic() - start) * 1000)
except Exception as e:
checks['connectivity'] = f'error: {e}'
ok = False
# Check 2: Cluster node count via info
try:
request = 'statistics'
policy = {'timeout': 5000}
info_map = aero_client.info_all(request, policy)
checks['nodeCount'] = len(info_map)
migrating = any(
'migrate_progress_send' in str(v) and
'migrate_progress_send=0' not in str(v)
for v in info_map.values()
)
checks['migrationsInProgress'] = migrating
checks['clusterStatus'] = 'migrating' if migrating else 'stable'
except Exception as e:
checks['clusterStatus'] = f'error: {e}'
ok = False
# Check 3: Namespace storage
namespace = os.environ.get('AEROSPIKE_NAMESPACE', 'test')
try:
ns_info = aero_client.info_all(f'namespace/{namespace}', {'timeout': 5000})
first_val = list(ns_info.values())[0] if ns_info else ''
info_str = str(first_val)
import re
used_match = re.search(r'device_used_bytes=(\d+)', info_str)
total_match = re.search(r'device_total_bytes=(\d+)', info_str)
stop_writes = 'stop_writes=true' in info_str
if used_match and total_match:
used = int(used_match.group(1))
total = int(total_match.group(1))
pct = round(used / total * 100, 1) if total > 0 else 0
checks['namespaceStoragePct'] = pct
max_pct = float(os.environ.get('MAX_STORAGE_PCT', '80'))
if pct > max_pct:
checks['namespaceHealth'] = f'storage high: {pct}%'
ok = False
else:
checks['namespaceHealth'] = 'ok'
if stop_writes:
checks['namespaceHealth'] = 'stop-writes active — namespace full'
ok = False
except Exception as e:
checks['namespaceHealth'] = f'error: {e}'
ok = False
status_code = 200 if ok else 503
return JSONResponse(status_code=status_code, content={
'status': 'ok' if ok else 'degraded',
'service': 'aerospike-service',
'checks': checks,
})
Go (aerospike-client-go)
// internal/health/handler.go
package health
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
as "github.com/aerospike/aerospike-client-go/v7"
)
func Handler(client *as.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
checks := map[string]any{}
ok := true
// Check 1: Write/read probe
start := time.Now()
key, _ := as.NewKey("test", "health", "vigilmon-probe")
bins := as.BinMap{"ts": time.Now().UnixMilli()}
writePolicy := as.NewWritePolicy(0, 0)
writePolicy.Timeout = 5 * time.Second
if err := client.Put(writePolicy, key, bins); err != nil {
checks["connectivity"] = "error: " + err.Error()
ok = false
} else {
readPolicy := as.NewPolicy()
readPolicy.Timeout = 5 * time.Second
if _, err := client.Get(readPolicy, key); err != nil {
checks["connectivity"] = "error: " + err.Error()
ok = false
} else {
checks["connectivity"] = "ok"
checks["connectivityLatencyMs"] = time.Since(start).Milliseconds()
}
}
// Check 2: Cluster node count
nodes := client.GetNodes()
checks["nodeCount"] = len(nodes)
if len(nodes) == 0 {
checks["clusterStatus"] = "no nodes connected"
ok = false
} else {
checks["clusterStatus"] = fmt.Sprintf("%d nodes connected", len(nodes))
}
// Check 3: Namespace storage via info
namespace := os.Getenv("AEROSPIKE_NAMESPACE")
if namespace == "" {
namespace = "test"
}
if len(nodes) > 0 {
infoMap, err := as.RequestNodeInfo(nodes[0], "namespace/"+namespace)
if err == nil {
infoStr := infoMap["namespace/"+namespace]
checks["namespaceInfoAvailable"] = len(infoStr) > 0
if len(infoStr) > 0 {
checks["namespaceHealth"] = "ok"
if contains(infoStr, "stop_writes=true") {
checks["namespaceHealth"] = "stop-writes active"
ok = false
}
}
}
}
w.Header().Set("Content-Type", "application/json")
if !ok {
w.WriteHeader(http.StatusServiceUnavailable)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"status": map[bool]string{true: "ok", false: "degraded"}[ok],
"service": "aerospike-service",
"checks": checks,
})
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 &&
func() bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}())
}
Deploy and verify the endpoint manually before wiring it into Vigilmon:
curl -i https://your-app.example.com/health
# HTTP/1.1 200 OK
# {"status":"ok","service":"aerospike-service","checks":{"connectivity":"ok","nodeCount":3,"clusterStatus":"stable","namespaceHealth":"ok"}}
Step 2: Configure the Vigilmon HTTP Monitor
- Log in to Vigilmon → Add Monitor → HTTP.
- URL: your service's health endpoint (e.g.
https://api.example.com/health). - Check interval: 60 seconds.
- Response timeout: 10 seconds.
- Expected status:
200. - JSON assertion: path
status, expected valueok. - Save the monitor.
Vigilmon probes from multiple geographic regions simultaneously. A single-region blip will not page you; multi-region consensus is required before opening an incident. This is particularly valuable for Aerospike because individual node failures may not impact all clients equally depending on your rack-awareness configuration.
Tip: Create a second monitor pointing to a
/health/detailedendpoint that returns node count and namespace storage percentage. Use a JSON assertion onchecks.namespaceStoragePctto alert before the namespace reaches stop-writes capacity.
Step 3: Heartbeat Monitoring for Aerospike-Backed Workers
Aerospike is commonly used as a session store, rate-limiter backend, or high-throughput event counter. Workers that depend on Aerospike will fail silently when the cluster is unavailable or when namespace writes are stopped. Vigilmon's heartbeat monitors detect this: your worker pings Vigilmon after each successful cycle, and if pings stop arriving, Vigilmon fires an alert.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat.
- Set the name:
aerospike-worker. - Set the expected interval: 2 minutes (adjust to your job frequency).
- Set the grace period: 5 minutes.
- Save — copy the unique heartbeat URL.
Wire It Into Your Worker
Node.js:
import Aerospike from 'aerospike';
import fetch from 'node-fetch';
async function runWorkerCycle() {
const key = new Aerospike.Key('events', 'queue', `job-${Date.now()}`);
await client.put(key, { processed: true, ts: Date.now() });
// Ping Vigilmon only after successful Aerospike write
await fetch(process.env.VIGILMON_HEARTBEAT_URL).catch(() => {});
}
setInterval(runWorkerCycle, 60_000);
Python:
import os, time, requests, aerospike
def run_worker_cycle():
key = ('events', 'queue', f'job-{int(time.time() * 1000)}')
client.put(key, {'processed': True, 'ts': int(time.time())})
# Only ping Vigilmon after successful Aerospike operation
requests.get(os.environ['VIGILMON_HEARTBEAT_URL'], timeout=5)
while True:
try:
run_worker_cycle()
except Exception as e:
print(f"Worker cycle failed — heartbeat NOT sent: {e}")
time.sleep(60)
Step 4: Cluster Migration Alerting
Aerospike performs data migrations when a node joins or leaves the cluster. Migrations are expected during maintenance but can last minutes to hours depending on data volume. During migrations, replication factor guarantees may be temporarily reduced.
Surface migration state in your health endpoint (as shown in Step 1) and create a separate Vigilmon monitor with an assertion on checks.migrationsInProgress:
false: cluster stable — greentrue: migrations in progress — Slack notification (P2, not a page)
This gives your team visibility into cluster rebalancing without triggering an on-call page for planned maintenance.
Step 5: Alerting Strategy
| Alert channel | When to use |
|---|---|
| PagerDuty | Connectivity check fails (cluster unreachable) |
| Slack webhook | Migrations detected (cluster rebalancing) |
| Slack + Email | Namespace storage > 80% (approaching stop-writes) |
| PagerDuty | stop_writes=true (writes rejected — active outage) |
| PagerDuty | Heartbeat missed (worker stopped processing) |
| Status page | Any user-facing service backed by Aerospike |
Set response time thresholds as an early warning:
- Alert at
10msfor direct Aerospike operation latency (sub-millisecond is normal; 10ms+ signals cluster pressure) - Alert at
500msfor health endpoint response time
What Vigilmon Catches That Internal Monitoring Misses
| Scenario | Internal check | Vigilmon |
|---|---|---|
| Namespace stop-writes active | Aerospike logs only | /health endpoint exposes flag |
| Node loss reduces replication factor | Cluster-internal event | Migration state surfaced in health |
| Client connection pool exhausted | No client-side metric | Health endpoint probe fails — 503 fires |
| Worker silently stopped writing | Process appears running | Heartbeat grace period expires → alert |
| Aerospike reachable from cluster but not app servers | Node-level check only | External HTTP probe from app's perspective |
| Monitoring pipeline (CloudWatch/Prometheus) fails | Alerts silently lost | Vigilmon is external — unaffected |
Aerospike's speed advantage disappears when your application cannot reach the cluster or when namespace writes are stopped. External health checks with Vigilmon give you the independent signal you need — before latency spikes reach your users.
Start monitoring your Aerospike applications in under 5 minutes — register free at vigilmon.online.