tutorial

Monitoring AWS MemoryDB with Vigilmon: Redis-Compatible In-Memory Health, Cluster Availability & Application-Layer Uptime Checks

Practical guide to uptime and health monitoring for AWS MemoryDB — HTTP health endpoints for MemoryDB-connected services, cluster and shard availability probing, replication lag detection, and independent external monitoring with Vigilmon.

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

  1. Log in to VigilmonAdd Monitor → HTTP.
  2. URL: your service's health endpoint (e.g. https://api.example.com/health).
  3. Check interval: 60 seconds.
  4. Response timeout: 10 seconds.
  5. Expected status: 200.
  6. JSON assertion: path status, expected value ok.
  7. 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

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat.
  2. Set the name: memorydb-queue-worker.
  3. Set the expected interval: 5 minutes (adjust to your job frequency).
  4. Set the grace period: 10 minutes.
  5. 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 20ms for PING latency (in-memory should be single-digit milliseconds in-region)
  • Alert at 100ms for 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →