Monitoring
Health endpoints, Prometheus metrics, status API, and Kubernetes probes for MemKV.
The MemKV server serves health endpoints and Prometheus metrics on its admin HTTP endpoint (default: 0.0.0.0:9901). The MemKV client has its own metrics exporters, which are off by default.
Client-side exporters
The MemKV client (the NIXL plugin and the LD_PRELOAD shim) reports request latency, transport state, and batch size. To turn the metrics on, set one of these environment variables. If neither is set, the client collects no metrics.
| Variable | Type | Default | Notes |
|---|---|---|---|
MEMKV_OTEL_ENDPOINT | URL | unset | OTLP/gRPC collector that receives spans and metrics (e.g. http://otel-collector:4317). Falls back to OTEL_EXPORTER_OTLP_ENDPOINT. The published packages do not include OTLP export. |
MEMKV_PROMETHEUS_BIND | host:port | unset | Address for a Prometheus scrape endpoint at /metrics inside the client process. Use it when the client runs in a long-lived process that Prometheus can reach. |
Each process starts one exporter. If MEMKV_OTEL_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT is set, the client uses OTLP and does not start the Prometheus endpoint. Because the published packages do not include OTLP export, use MEMKV_PROMETHEUS_BIND, and make sure neither OTLP variable is set in the client's environment.
Health endpoints
All admin endpoints are versioned under /v1/.
| Endpoint | Purpose | Response |
|---|---|---|
GET /v1/health | Liveness probe | JSON {status, rdma_active, drives_online, drives_offline, drives_total} (macOS: {status} only) |
GET /v1/ready | Readiness probe | ok (200), or 503 license expired once the installed license is fully expired, when the server rejects every request |
GET /v1/status | Detailed status | JSON with memory, storage, RDMA stats |
GET /v1/metrics | Prometheus metrics | text/plain Prometheus format |
curl http://localhost:9901/v1/health
# {"status":"healthy","rdma_active":true,"drives_online":12,"drives_offline":0,"drives_total":12}status is healthy, degraded, or unhealthy. The HTTP status code
matches: 200 for healthy, 503 for unhealthy.
memkv admin status --json returns the same fields as /v1/status below,
including the storage totals (raw_bytes_total, usable_bytes_total,
used_bytes_total) and the byte fields for each device. memkv admin status
without --json shows storage use as used / usable and a percentage.
Status endpoint
curl http://localhost:9901/v1/status{
"memory": {
"used_bytes": 1073741824,
"total_bytes": 265289728000,
"slabs_allocated": 512,
"slabs_total": 126464
},
"storage": {
"num_devices": 24,
"block_size_bytes": 4194304,
"total_blocks": 1000,
"raw_bytes_total": 384000000000000,
"usable_bytes_total": 383979687936000,
"used_bytes_total": 176160768,
"devices": [
{
"device_id": 0,
"path": "/dev/nvme0n1",
"total_slots": 3815461,
"allocated_slots": 42,
"raw_capacity_bytes": 16000000000000,
"usable_bytes": 15999153635328,
"metadata_bytes": 846364672,
"used_bytes": 176160768
}
]
},
"rdma": {
"active_connections": 8
},
"blocks_total": 1000,
"blocks_persisted": 950,
"uptime_seconds": 3612
}Prometheus metrics
Request operations
| Metric | Type | Description |
|---|---|---|
memkv_allocate_total | Counter | Total allocate requests |
memkv_lookup_total | Counter | Total lookup requests |
memkv_delete_total | Counter | Total delete requests |
memkv_commit_total | Counter | Total commit requests |
memkv_read_total | Counter | Total direct-IO read requests |
memkv_read_bytes_total | Counter | Total bytes served on direct-IO reads |
memkv_write_total | Counter | Total direct-IO write requests |
memkv_write_bytes_total | Counter | Total bytes persisted on direct-IO writes |
memkv_connect_total | Counter | Total RDMA connect requests |
memkv_exists_total | Counter | Total exists requests |
memkv_not_found_total | Counter | Total not-found responses |
memkv_no_space_total | Counter | Total no-space responses |
memkv_invalid_requests_total | Counter | Total invalid/malformed requests |
memkv_request_latency_us | Histogram | End-to-end request latency |
RDMA transport
| Metric | Type | Description |
|---|---|---|
memkv_rdma_active | Gauge | 1 if RDMA data path is active, 0 if fell back to local-only |
memkv_rdma_connections_active | Gauge | Active RDMA connections |
memkv_rdma_put_latency_us | Histogram | RDMA put latency (server RDMA read from client on a put) |
memkv_rdma_get_latency_us | Histogram | RDMA get latency (server RDMA write to client on a get) |
memkv_rdma_errors_total | Counter | RDMA transport errors (CQ/post failures) |
TCP transport
| Metric | Type | Description |
|---|---|---|
memkv_tcp_put_total | Counter | TCP put data frames served (inline put or streamed chunk) |
memkv_tcp_get_total | Counter | TCP get data frames served (inline get or streamed chunk) |
memkv_tcp_put_bytes_total | Counter | Payload bytes accepted by TCP puts |
memkv_tcp_get_bytes_total | Counter | Payload bytes served by TCP gets |
memkv_tcp_put_latency_us | Histogram | Server-side TCP put handling latency (excludes socket write) |
memkv_tcp_get_latency_us | Histogram | Server-side TCP get handling latency (excludes socket write) |
The latency histograms do not include time spent waiting on the network throttle. They measure only the server's own work.
Network throttle
The server always exports these metrics. When network.throttle is off, they stay at zero.
| Metric | Type | Description |
|---|---|---|
memkv_throttle_bytes_total | Counter | Bulk bytes metered against the cap, RDMA and TCP combined |
memkv_throttle_delay_seconds_total | Counter | Cumulative seconds transfers spent waiting on the cap |
memkv_throttle_waits_total | Counter | Transfers the cap delayed; divide the delay total by this for mean wait |
Useful queries:
# Is the cap binding right now? Seconds of delay per second of wall time.
rate(memkv_throttle_delay_seconds_total[1m])
# Achieved bulk rate in bits/s, to compare against your configured cap.
rate(memkv_throttle_bytes_total[1m]) * 8
# Mean delay per throttled transfer.
rate(memkv_throttle_delay_seconds_total[5m])
/ rate(memkv_throttle_waits_total[5m])If latency is high and the first query stays at zero, the throttle is not the cause. Check the drives.
Storage devices
| Metric | Type | Labels | Description |
|---|---|---|---|
memkv_device_slots_used | Gauge | device | Allocated slots per device |
memkv_device_capacity_raw_bytes | Gauge | device | Raw NVMe capacity per device in bytes |
memkv_device_capacity_usable_bytes | Gauge | device | Per-device bytes available for user data (raw minus MemKV metadata regions) |
memkv_device_bytes_used | Gauge | device | Per-device bytes currently allocated to blocks (allocated_slots * block_size) |
memkv_storage_raw_bytes | Gauge | — | Sum of raw NVMe capacity across all devices |
memkv_storage_usable_bytes | Gauge | — | Sum of usable capacity (data region only) across all devices |
memkv_storage_used_bytes | Gauge | — | Sum of bytes allocated to blocks across all devices |
memkv_trim_extents_total | Counter | device | Extents trimmed per device |
memkv_trim_bytes_total | Counter | device | Bytes trimmed per device |
memkv_trim_latency_us | Histogram | device | TRIM batch latency |
memkv_trim_errors_total | Counter | device | TRIM errors per device |
memkv_trim_pending | Gauge | — | Pending TRIM requests |
Capacity reclaim
| Metric | Type | Description |
|---|---|---|
memkv_storage_evictions_total | Counter | Blocks reclaimed from storage, least recently used first |
memkv_storage_evictions_unindexed_total | Counter | Blocks reclaimed oldest-first because they had no recency to sort by |
memkv_storage_evicted_bytes_total | Counter | Capacity reclaimed, charged at allocation granularity |
memkv_storage_eviction_passes_total | Counter | Reclaim passes run |
memkv_storage_eviction_starved_passes_total | Counter | Passes that found nothing to reclaim while still above the high watermark |
memkv_resident_evictions_total | Counter | Blocks whose memory slab was released under memory pressure; the bytes stay durable |
What to watch:
- Alert when
memkv_storage_eviction_starved_passes_totalrises. Storage is above its high watermark and nothing is left to reclaim, so writes will soon fail. Add capacity, or find what is holding the blocks. - Fill ratio:
memkv_storage_used_bytes / memkv_storage_usable_bytes. Normally it moves between the two watermarks. If it stays at the high watermark, reclaim cannot keep up with writes. memkv_storage_evictions_unindexed_totalrising steadily: the server is reclaiming blocks oldest first, because they were stored before the last restart and have not been read since. This is expected for a while after a restart on large drives, and it slows as clients read their working set. If it does not slow, the server is reclaiming blocks without regard to which ones are in use.- Passes per eviction: if
memkv_storage_eviction_passes_totalrises much faster thanmemkv_storage_evictions_total, reclaim passes run but find little. Widen the gap between the watermarks.
Memory and blocks
| Metric | Type | Description |
|---|---|---|
memkv_blocks_total | Gauge | Total blocks in index |
memkv_blocks_persisted | Gauge | Blocks persisted to storage |
memkv_blocks_by_state | Gauge | Block count by state |
memkv_slabs_allocated | Gauge | Allocated memory slabs |
memkv_slabs_total | Gauge | Total memory slabs |
memkv_memory_used_bytes | Gauge | Memory used for blocks |
memkv_memory_total_bytes | Gauge | Total memory available |
memkv_uptime_seconds | Gauge | Server uptime in seconds (since process start) |
Admin API
| Metric | Type | Labels | Description |
|---|---|---|---|
memkv_admin_requests_total | Counter | path | Admin endpoint requests by path |
Storage pipeline (hot-path tracing)
Detailed timings inside the write path. Use them to diagnose slow writes under sustained load.
| Metric | Type | Description |
|---|---|---|
memkv_persist_us | Histogram | Direct-write end-to-end wall time (request entry → persist return) |
memkv_persist_pre_wait_us | Histogram | Entry to just before the writer-worker handoff (extent lock, journal Alloc, …) |
memkv_writer_wait_us | Histogram | Wait time on the io_uring writer-worker channel |
memkv_persist_post_wait_us | Histogram | After writer-worker completion to persist return (journal Commit, dirty queue) |
memkv_iouring_write_us | Histogram | io_uring write submit-to-completion latency |
memkv_iouring_fsync_us | Histogram | io_uring fsync submit-to-completion latency |
memkv_iouring_fsync_batch_size | Histogram | Data writes drained per fsync completion |
memkv_allocate_duration_us | Histogram | FreeList slot allocate time |
memkv_journal_flush_duration_us | Histogram | Journal buffer flush (fdatasync) time |
memkv_journal_pending_entries | Gauge | Un-flushed journal entries across devices |
memkv_dirty_queue_depth | Gauge | Pending entries in per-device dirty queues |
memkv_btree_cache_hits_total | Counter | BTree leaf cache hits |
memkv_btree_cache_misses_total | Counter | BTree leaf cache misses |
memkv_btree_leaf_miss_us | Histogram | BTree leaf miss-path resolution time |
memkv_freelist_cas_retries_total | Counter | FreeList head CAS retry count |
memkv_slab_double_free_total | Counter | Bounce-buffer slabs freed more than once |
Timings for each request stage, labeled op and step:
| Metric | Type | Labels | Description |
|---|---|---|---|
memkv_server_step_seconds | Histogram | op, step | Time spent in one server-side hot-path step |
memkv_server_step_count | Counter | op, step | Times that step ran |
memkv_server_step_bytes_total | Counter | op, step | Payload bytes through that step (omitted where nil) |
The client metric memkv_client_step_seconds uses the same op and step
labels. To find where a slow put spends its time, compare
memkv_server_step_seconds{op="commit",step="total"} (time on the server)
with memkv_client_step_seconds{op="put",step="commit"} (time including the
network).
Kubernetes probes
livenessProbe:
httpGet:
path: /v1/health
port: 9901
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /v1/ready
port: 9901
initialDelaySeconds: 5
periodSeconds: 5