AWS MemoryDB for Redis is a durable, Redis-compatible in-memory database with Multi-AZ replication and strong consistency — but your application's uptime depends on more than MemoryDB's infrastructure guarantees. Cluster endpoint routing during failover, shard replication lag, client-side connection pool exhaustion, and VPC security group changes can all degrade your application silently while Amazon CloudWatch metrics remain within bounds. Vigilmon adds an independent external layer: HTTP health checks on your MemoryDB-connected services that probe real cluster connectivity, surface replication state, and alert on application-level failures before users notice.
This tutorial shows you how to build meaningful monitoring for MemoryDB-backed applications using Vigilmon.
What You'll Build
- A health endpoint that probes real MemoryDB cluster connectivity
- Shard and replication health checks
- A Vigilmon HTTP monitor with appropriate timeout and assertion settings
- A heartbeat monitor for MemoryDB-backed background workers
- A cluster failover-aware alerting strategy
Prerequisites
- AWS account with a MemoryDB cluster (single-shard dev or multi-shard production)
- An application deployed in the same VPC as your MemoryDB cluster
- A free account at vigilmon.online
Step 1: Add a Health Endpoint to Your MemoryDB Service
MemoryDB uses the Redis protocol, so standard Redis clients work. MemoryDB requires TLS connections in production. Add a /health endpoint that probes real cluster connectivity and checks replication state.
Node.js (ioredis)
// routes/health.js
import Redis from 'ioredis';
// MemoryDB cluster endpoint — always use the cluster DNS name
const cluster = new Redis.Cluster(
[{ host: process.env.MEMORYDB_CLUSTER_ENDPOINT, port: 6379 }],
{
dnsLookup: (address, callback) => callback(null, address),
redisOptions: {
tls: {}, // MemoryDB requires TLS
connectTimeout: 5000,
commandTimeout: 3000,
},
clusterRetryStrategy: () => null, // Don't retry on health checks
}
);
export async function healthHandler(req, res) {
const checks = {};
let ok = true;
const start = Date.now();
// Check 1: Basic PING — verifies cluster endpoint routing
try {
const pong = await cluster.ping();
checks.ping = pong === 'PONG' ? 'ok' : 'unexpected response';
checks.pingLatencyMs = Date.now() - start;
if (pong !== 'PONG') ok = false;
} catch (err) {
checks.ping = `error: ${err.message}`;
ok = false;
}
// Check 2: Write/read round-trip — verifies durability path
try {
const writeStart = Date.now();
const key = `vigilmon:health:${Date.now()}`;
await cluster.set(key, '1', 'EX', 30); // 30-second TTL
const val = await cluster.get(key);
if (val !== '1') {
checks.readWrite = 'inconsistency — read returned unexpected value';
ok = false;
} else {
checks.readWrite = 'ok';
checks.readWriteLatencyMs = Date.now() - writeStart;
}
await cluster.del(key);
} catch (err) {
checks.readWrite = `error: ${err.message}`;
ok = false;
}
// Check 3: Cluster INFO — node count and replication role
try {
// Get info from any primary node
const info = await cluster.call('INFO', 'replication');
const infoStr = String(info);
const role = infoStr.match(/role:(\w+)/)?.[1];
const connectedSlaves = parseInt(infoStr.match(/connected_slaves:(\d+)/)?.[1] || '0');
checks.role = role || 'unknown';
checks.connectedReplicas = connectedSlaves;
// A primary with no connected replicas loses HA durability
if (role === 'master' && connectedSlaves === 0 && process.env.NODE_ENV === 'production') {
checks.replication = 'primary has no connected replicas — HA degraded';
// Not critical (writes still work) but worth alerting
} else {
checks.replication = 'ok';
}
} catch (err) {
checks.replication = `error: ${err.message}`;
ok = false;
}
res.status(ok ? 200 : 503).json({
status: ok ? 'ok' : 'degraded',
service: 'memorydb-service',
checks,
});
}
Python (redis-py with cluster support)
# routers/health.py
import os, time, ssl
from redis.cluster import RedisCluster
from redis.exceptions import RedisError
from fastapi import APIRouter
from fastapi.responses import JSONResponse
router = APIRouter()
ssl_context = ssl.create_default_context()
client = RedisCluster(
host=os.environ['MEMORYDB_CLUSTER_ENDPOINT'],
port=6379,
ssl=True,
ssl_context=ssl_context,
socket_timeout=5,
socket_connect_timeout=5,
decode_responses=True,
)
@router.get("/health")
async def health():
checks = {}
ok = True
start = time.monotonic()
# Check 1: PING
try:
pong = client.ping()
checks['pingLatencyMs'] = int((time.monotonic() - start) * 1000)
checks['ping'] = 'ok' if pong else 'no response'
if not pong:
ok = False
except Exception as e:
checks['ping'] = f'error: {e}'
ok = False
# Check 2: Write/read round-trip
try:
write_start = time.monotonic()
key = f'vigilmon:health:{int(time.time() * 1000)}'
client.set(key, '1', ex=30)
val = client.get(key)
if val != '1':
checks['readWrite'] = 'inconsistency detected'
ok = False
else:
checks['readWrite'] = 'ok'
checks['readWriteLatencyMs'] = int((time.monotonic() - write_start) * 1000)
client.delete(key)
except Exception as e:
checks['readWrite'] = f'error: {e}'
ok = False
# Check 3: Replication info
try:
info = client.info('replication')
role = info.get('role', 'unknown')
replicas = info.get('connected_slaves', 0)
checks['role'] = role
checks['connectedReplicas'] = replicas
if role == 'master' and replicas == 0 and os.environ.get('ENV') == 'production':
checks['replication'] = 'no replicas connected — HA degraded'
else:
checks['replication'] = 'ok'
except Exception as e:
checks['replication'] = 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': 'memorydb-service',
'checks': checks,
})
Go (go-redis with cluster support)
// internal/health/handler.go
package health
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/redis/go-redis/v9"
)
func Handler(cluster *redis.ClusterClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
checks := map[string]any{}
ok := true
ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
defer cancel()
// Check 1: PING
start := time.Now()
if err := cluster.Ping(ctx).Err(); err != nil {
checks["ping"] = "error: " + err.Error()
ok = false
} else {
checks["ping"] = "ok"
checks["pingLatencyMs"] = time.Since(start).Milliseconds()
}
// Check 2: Write/read round-trip
writeStart := time.Now()
key := fmt.Sprintf("vigilmon:health:%d", time.Now().UnixMilli())
if err := cluster.Set(ctx, key, "1", 30*time.Second).Err(); err != nil {
checks["readWrite"] = "error writing: " + err.Error()
ok = false
} else {
val, err := cluster.Get(ctx, key).Result()
cluster.Del(ctx, key)
if err != nil || val != "1" {
checks["readWrite"] = "error reading or inconsistency"
ok = false
} else {
checks["readWrite"] = "ok"
checks["readWriteLatencyMs"] = time.Since(writeStart).Milliseconds()
}
}
// Check 3: Cluster info via ForEachMaster
masterCount := 0
err := cluster.ForEachMaster(ctx, func(ctx context.Context, c *redis.Client) error {
masterCount++
return nil
})
if err != nil {
checks["clusterTopology"] = "error: " + err.Error()
ok = false
} else {
checks["masterCount"] = masterCount
checks["clusterTopology"] = "ok"
}
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": "memorydb-service",
"checks": checks,
})
}
}
func NewCluster(endpoint string) *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{endpoint + ":6379"},
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
})
}
Deploy and verify the endpoint before configuring Vigilmon:
curl -i https://your-app.example.com/health
# HTTP/1.1 200 OK
# {"status":"ok","service":"memorydb-service","checks":{"ping":"ok","pingLatencyMs":2,"readWrite":"ok","readWriteLatencyMs":5,"replication":"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, requiring consensus before opening an incident. This prevents false-positive pages during MemoryDB automatic failover events, which typically complete in under 30 seconds.
MemoryDB vs ElastiCache note: MemoryDB is a durable database (not just a cache) — data is persisted to a Multi-AZ transaction log. Your health checks should verify both read-write round-trips and replication state, not just PING/PONG. A PING response from a degraded cluster with broken replication would otherwise appear healthy.
Step 3: Heartbeat Monitoring for MemoryDB-Backed Workers
Workers using MemoryDB as a job queue (via LPUSH/BRPOP or XADD/XREADGROUP streams) will stall silently during failover when the cluster endpoint resolves to a new primary. Vigilmon's heartbeat monitors detect this by requiring your worker to ping Vigilmon after each successful processing cycle.
Set Up the Heartbeat Monitor
- In Vigilmon, go to Monitors → New Monitor → Heartbeat.
- Set the name:
memorydb-queue-worker. - Set the expected interval: 5 minutes (adjust to your job frequency).
- Set the grace period: 10 minutes.
- Save — copy the unique heartbeat URL.
Wire It Into Your Worker
Node.js (Redis Streams):
import Redis from 'ioredis';
import fetch from 'node-fetch';
const client = new Redis({
host: process.env.MEMORYDB_CLUSTER_ENDPOINT,
port: 6379,
tls: {},
});
async function runStreamWorker() {
const results = await client.xreadgroup(
'GROUP', 'workers', 'consumer-1',
'COUNT', '10',
'BLOCK', '5000',
'STREAMS', 'jobs', '>'
);
if (results) {
for (const [stream, messages] of results) {
for (const [id, fields] of messages) {
await processMessage(id, fields);
await client.xack('jobs', 'workers', id);
}
}
// Only ping after successful processing
await fetch(process.env.VIGILMON_HEARTBEAT_URL).catch(() => {});
}
}
// Run continuously
(async function loop() {
while (true) {
try {
await runStreamWorker();
} catch (err) {
console.error('Stream worker failed — heartbeat NOT sent:', err.message);
await new Promise(r => setTimeout(r, 5000));
}
}
})();
Python (Redis Streams):
import os, time, requests
from redis.cluster import RedisCluster
import ssl
ssl_context = ssl.create_default_context()
client = RedisCluster(
host=os.environ['MEMORYDB_CLUSTER_ENDPOINT'],
port=6379, ssl=True, ssl_context=ssl_context,
decode_responses=True,
)
def run_stream_worker():
results = client.xreadgroup(
'workers', 'consumer-1',
{'jobs': '>'},
count=10, block=5000,
)
if results:
for stream, messages in results:
for msg_id, fields in messages:
process_message(msg_id, fields)
client.xack('jobs', 'workers', msg_id)
# Only ping after successful processing
requests.get(os.environ['VIGILMON_HEARTBEAT_URL'], timeout=5)
while True:
try:
run_stream_worker()
except Exception as e:
print(f"Stream worker failed — heartbeat NOT sent: {e}")
time.sleep(5)
Step 4: MemoryDB Shard and Failover Monitoring
MemoryDB distributes data across shards, each with a primary and up to five replicas. A shard primary failure triggers automatic promotion of a replica within seconds.
Monitor shard health by checking all cluster nodes in your health endpoint:
// Extended health check — shard topology
export async function shardHealthHandler(req, res) {
const nodes = cluster.nodes('all');
const shardStatus = [];
for (const node of nodes) {
try {
const info = await node.info('replication');
const role = info.match(/role:(\w+)/)?.[1];
const host = node.options.host;
shardStatus.push({ host, role, status: 'ok' });
} catch (err) {
shardStatus.push({ host: node.options.host, role: 'unknown', status: 'error' });
}
}
const primaryCount = shardStatus.filter(n => n.role === 'master').length;
const unhealthyNodes = shardStatus.filter(n => n.status === 'error').length;
res.status(unhealthyNodes === 0 ? 200 : 503).json({
status: unhealthyNodes === 0 ? 'ok' : 'degraded',
primaryCount,
unhealthyNodes,
nodes: shardStatus,
});
}
Step 5: Alerting Strategy
| Alert channel | When to use | |---|---| | PagerDuty | PING fails (cluster unreachable) | | PagerDuty | Read/write round-trip fails (durability path broken) | | Slack webhook | Replica disconnected in production (HA degraded) | | PagerDuty | Heartbeat missed (stream worker stopped processing) | | Slack webhook | Read/write latency > 50ms (cluster under pressure) | | Status page | Any user-facing service backed by MemoryDB |
Set response time thresholds in Vigilmon as an early warning:
- Alert at
20msfor PING latency (in-memory should be single-digit milliseconds in-region) - Alert at
100msfor health endpoint response time (includes application overhead)
What Vigilmon Catches That CloudWatch Misses
| Scenario | CloudWatch | Vigilmon |
|---|---|---|
| Cluster endpoint routing fails after failover | No application-layer check | Health endpoint probe fails → alert |
| TLS certificate rotation breaks connections | No connection-level check | Write/read probe catches TLS errors |
| VPC security group blocks client access | No network path check | External HTTP probe catches 503 |
| Stream worker stalled during failover | No worker-level check | Heartbeat grace period expires → alert |
| Shard replication broken (HA degraded) | ReplicationLag metric exists | Health endpoint surfaces replica count |
| CloudWatch alarm SNS delivery fails | Alerts silently lost | Vigilmon is external — unaffected |
| Client connection pool exhausted | No pool-level metric | Health endpoint probe fails → 503 |
MemoryDB's durability guarantee is only as useful as your ability to detect when the cluster is degraded. External health checks from Vigilmon give you the independent signal you need — catching failures that CloudWatch metrics never surface.
Start monitoring your MemoryDB applications in under 5 minutes — register free at vigilmon.online.