Real-Time Metrics Visualization

July 15, 2026 · View on GitHub

memtier_benchmark supports streaming real-time metrics to a StatsD-compatible server for live visualization during benchmark runs. This is particularly useful for:

  • Observing performance during long-running tests
  • Monitoring latency spikes during scaling events (e.g., Redis Enterprise slot migrations)
  • Comparing multiple benchmark runs side-by-side
  • Sharing live dashboards with team members

Quick Start

1. Start the Monitoring Stack

A pre-configured Docker Compose setup is included with Graphite (StatsD receiver) and Grafana:

docker-compose -f docker-compose.statsd.yml up -d

This starts:

  • Graphite + StatsD on localhost:8125 (UDP) - receives metrics
  • Grafana on http://localhost:3000 - visualization (login: admin / admin)

2. Run a Benchmark with Metrics

./memtier_benchmark -s <redis-host> -p <redis-port> \
    --statsd-host=localhost \
    --test-time=60

3. View the Dashboard

Open http://localhost:3000 in your browser, log in with admin/admin, and navigate to the Memtier Benchmark dashboard. You'll see live metrics updating in real-time.

4. Stop the Monitoring Stack

docker-compose -f docker-compose.statsd.yml down

To also remove stored data:

docker-compose -f docker-compose.statsd.yml down -v

Command-Line Options

OptionDefaultDescription
--statsd-host=HOST(disabled)StatsD server hostname. Metrics are only sent when this is set.
--statsd-port=PORT8125StatsD server UDP port.
--statsd-prefix=PREFIXmemtierPrefix for all metric names in Graphite.
--statsd-run-label=LABELdefaultLabel to identify this benchmark run. Use different labels to compare runs.
--graphite-port=PORT8080Graphite HTTP port for event annotations. Use 80 when running inside the same Docker network.

Examples

Basic usage:

./memtier_benchmark -s redis.example.com --statsd-host=localhost --test-time=120

With a custom run label for comparison:

./memtier_benchmark -s redis.example.com --statsd-host=localhost \
    --statsd-run-label=baseline --test-time=60

# Later, run another test with a different label
./memtier_benchmark -s redis.example.com --statsd-host=localhost \
    --statsd-run-label=after-upgrade --test-time=60

Custom prefix (useful if sharing a Graphite instance):

./memtier_benchmark -s redis.example.com --statsd-host=metrics.internal \
    --statsd-prefix=team1.memtier --statsd-run-label=prod-test

Metrics Reference

The following metrics are sent approximately every 1 second during the benchmark.

Throughput

MetricStatsD typeGraphite pathDescription
ops_secgaugestats.gauges.<prefix>.<label>.ops_secInstantaneous ops/sec over the last interval
ops_sec_avggaugestats.gauges.<prefix>.<label>.ops_sec_avgRunning average ops/sec since benchmark start
bytes_secgaugestats.gauges.<prefix>.<label>.bytes_secInstantaneous byte throughput over the last interval
bytes_sec_avggaugestats.gauges.<prefix>.<label>.bytes_sec_avgRunning average byte throughput since benchmark start

Latency

MetricStatsD typeGraphite pathDescription
latency_mstiming (ms)stats.timers.<prefix>.<label>.latency_ms.*Instantaneous average latency (ms) over the last interval
latency_avg_mstiming (ms)stats.timers.<prefix>.<label>.latency_avg_ms.*Running average latency (ms) since benchmark start
latency_p<N>gaugestats.gauges.<prefix>.<label>.latency_p<N>Instantaneous latency at percentile N (ms). One metric per percentile configured via --print-percentiles. Default: latency_p50, latency_p99, latency_p99_9. Decimal points are replaced with underscores (e.g. latency_p99_9 for p99.9).

Note: latency_ms and latency_avg_ms are sent as StatsD timing metrics (ms type). StatsD processes them into derived stats (mean, upper, lower, etc.) that appear under stats.timers.* in Graphite — not under stats.gauges.* like the other metrics. The per-percentile latency_p<N> metrics are plain gauges and appear under stats.gauges.*.

Connections and Errors

MetricStatsD typeGraphite pathDescription
connectionsgaugestats.gauges.<prefix>.<label>.connectionsActive connection count (--clients × active thread count)
connection_errorsgaugestats.gauges.<prefix>.<label>.connection_errorsCumulative connection error count. Only sent when the count is > 0 and not zeroed at run end — stale values may linger in Graphite after errors clear. Protocol-level command errors are not tracked here.

Progress

MetricStatsD typeGraphite pathDescription
progress_pctgaugestats.gauges.<prefix>.<label>.progress_pctBenchmark completion percentage (0–100)

Events (Graphite annotations)

Two events are sent via HTTP POST to the Graphite events API (not StatsD UDP):

EventTagsWhen
Benchmark Startedmemtier,startImmediately before the benchmark loop begins
Benchmark Completedmemtier,endImmediately after all threads finish

These appear as vertical annotation lines on the Grafana dashboard.

End-of-run zeroing

When the benchmark completes, the following gauges are explicitly zeroed so graphs return to baseline rather than holding the last value: ops_sec, ops_sec_avg, bytes_sec, bytes_sec_avg, progress_pct.

connections, latency_ms, latency_avg_ms, latency_p<N>, and connection_errors are not zeroed at run end.

Comparing Multiple Benchmark Runs

One of the most powerful features is the ability to overlay multiple benchmark runs on the same graphs for direct comparison. This is ideal for A/B testing, before/after comparisons, or evaluating different configurations.

How It Works

  1. Run benchmarks with different labels:

    # First run - baseline
    ./memtier_benchmark -s redis-server --statsd-host=localhost \
        --statsd-run-label=baseline --test-time=60
    
    # Second run - after changes
    ./memtier_benchmark -s redis-server --statsd-host=localhost \
        --statsd-run-label=after-tuning --test-time=60
    
    # Third run - different configuration
    ./memtier_benchmark -s redis-server --statsd-host=localhost \
        --statsd-run-label=high-concurrency -c 100 -t 8 --test-time=60
    
  2. Use the Run Label dropdown at the top of the Grafana dashboard to:

    • Select multiple labels - Hold Ctrl/Cmd and click to select several runs
    • Select "All" - Overlay every run on the same graphs
    • Deselect runs - Click to toggle individual runs on/off
  3. Compare visually - Each run appears as a separate line with its own color, making it easy to spot performance differences.

Example Use Cases

ScenarioLabels to Compare
Before/after Redis upgraderedis-6.2, redis-7.0
Connection pool tuningpool-10, pool-50, pool-100
Cluster scaling test3-shards, 6-shards, 12-shards
Network latency impactsame-az, cross-az, cross-region

Grafana Dashboard

The included dashboard provides:

  • Operations per Second - Current and average ops/sec over time
  • Latency - Current and average latency in milliseconds
  • Throughput - Current and average bytes/sec
  • Connections - Active connection count
  • Progress - Benchmark completion percentage
  • Connection Errors - Error count indicator

All panels support multi-run overlay when multiple run labels are selected.

Troubleshooting

Verify StatsD is Receiving Metrics

Send a test metric manually:

echo "test.metric:100|g" | nc -u -w1 localhost 8125

Check Graphite's web UI at http://localhost:8080 to see if metrics appear.

No Data in Grafana

  1. Ensure the benchmark is running with --statsd-host set
  2. Check that port 8125/UDP is accessible (not blocked by firewall)
  3. Verify Grafana's Graphite datasource is configured (should be automatic with the Docker setup)
  4. Try refreshing the dashboard or adjusting the time range to "Last 5 minutes"

Metrics Delayed or Missing

  • StatsD aggregates metrics every 1 second by default
  • The dashboard refreshes every 1 second
  • If running memtier from a container, use --statsd-host=host.docker.internal (macOS/Windows) or the host's IP

Reset Stored Data

To clear all historical metrics and start fresh:

docker-compose -f docker-compose.statsd.yml down -v
docker-compose -f docker-compose.statsd.yml up -d

Architecture

┌─────────────────────┐     UDP:8125      ┌─────────────────────┐
│ memtier_benchmark   │ ───────────────── │ Graphite + StatsD   │
│ --statsd-host=...   │                   │ (metrics storage)   │
└─────────────────────┘                   └──────────┬──────────┘

                                          ┌──────────▼──────────┐
                                          │      Grafana        │
                                          │   localhost:3000    │
                                          └─────────────────────┘

Prometheus /metrics Exporter

In addition to the StatsD/Graphite push transport above, memtier_benchmark can expose the same live metrics over a Prometheus /metrics HTTP endpoint (pull model). The exporter is compiled in by default and enabled at runtime by --prometheus-port.

# Fixed port (loopback by default):
./memtier_benchmark -s redis-server --test-time=60 --prometheus-port=8080
curl http://127.0.0.1:8080/metrics

# Ephemeral port — the bound URL is announced on stdout as a single line:
./memtier_benchmark -s redis-server --test-time=60 --prometheus-port=0
# Prometheus exporter listening on http://127.0.0.1:43217/metrics

Command-Line Options (Prometheus)

OptionDefaultDescription
--prometheus-port=PORT(disabled)TCP port for /metrics. 0 selects an ephemeral port. Setting it enables the exporter.
--prometheus-bind-addr=ADDR127.0.0.1Numeric IPv4/IPv6 address to bind to. Hostnames are not supported. A non-loopback address prints a one-shot warning.
--prometheus-run-label=KEY=VALUE(none)Constant label applied to every sample; repeatable, max 16.
--prometheus-latency-buckets=LIST(26 built-in)Comma-separated, strictly ascending latency bucket bounds in seconds. Bounds within ~1% of each other collapse to one HDR slot and are rejected.

Ephemeral-port contract

With --prometheus-port=0 the kernel assigns a free port; memtier prints the fully-resolved URL exactly once on stdout:

Prometheus exporter listening on http://127.0.0.1:43217/metrics

IPv6 binds are bracketed (http://[::1]:43217/metrics). Automation should parse this line rather than guess a port. The endpoint serves GET /metrics only; other methods return 501, unknown paths return a fixed 404 body, and the response carries Content-Type: text/plain; version=0.0.4; charset=utf-8.

Metric Reference and StatsD Mapping

The exporter publishes the same one-producer snapshot as StatsD, reshaped to Prometheus conventions (cumulative counters, native histogram). Notable differences from the StatsD names above:

PrometheusTypeStatsD equivalentNotes
memtier_ops_totalcounterops_sec (rate)Prometheus exports the cumulative total; rate is derived at query time.
memtier_sent_bytes_total / memtier_received_bytes_totalcounterbytes_secCumulative bytes since process start.
memtier_hits_total / memtier_misses_totalcounterhits/misses
memtier_errors_totalcounterErrors after retries are exhausted.
memtier_connection_errors_totalcounterconnection_errorsAccumulated across runs (StatsD reports the raw per-run value).
memtier_retry_attempts_total / memtier_retried_ops_totalcounterNonzero only with --retry-on-error.
memtier_connections / memtier_threadsgaugeconnections
memtier_run / memtier_configured_runsgaugeCurrent run (1-based; 0 before the first run) and --run-count.
memtier_config_test_time_secondsgaugeConfigured --test-time (0 when bounded by --requests).
memtier_latency_secondshistogramlatency_ms familySeconds, not milliseconds; see the accuracy notes below.
memtier_build_info{version,git_sha}gauge=1Build identity only — no config labels.
memtier_start_time_secondsgaugeProcess start, Unix seconds.
memtier_exporter_renders_totalcounterNumber of times the body was rendered (scrapes that missed the 1 s render cache).
memtier_exporter_snapshot_age_secondsgaugeSeconds since the benchmark loop last published a snapshot.

Counters accumulate across runs and connection restarts, so a multi-run session (--run-count > 1) produces monotonically non-decreasing series instead of the per-run reset that a naive exporter would emit.

Latency accuracy (error model)

memtier_latency_seconds is built from 1 Hz snapshots of each connection's in-progress second of HDR-recorded latencies. Treat it as an operational signal, not the authoritative result:

  • Phase-dependent capture (~50% in steady state). Only the portion of each connection's per-second histogram that is complete at snapshot time is folded in, so in steady state roughly half of all operations are represented. memtier_latency_seconds_count is therefore not comparable to memtier_ops_total and must not be used to compute a hit fraction.
  • Stall dedup + residuals. A snapshot whose source histogram has not changed since the previous tick is skipped, so a stalled server (every in-flight op frozen) does not keep inflating _count; once traffic resumes the series grows again. A small residual at the tail of a run can be lost this way.
  • Run-tail loss. The final fraction of a second at run end may not be captured by the last tick.
  • HDR quantization (≤1%). Bucket placement carries up to 1% HDR quantization; the le bound rendered is the user-supplied (µs-quantized) bound, not the internal slot edge.

For exact percentiles and totals, use the end-of-run output (or the full latency spectrum, see the README). The exporter is for live observation.

Security / threat model

The /metrics body is designed to leak zero connection or configuration identity. It contains no server address or port, no password or --authenticate credentials, no request URI, and no filesystem paths (including TLS cert/key/CA paths). Prometheus's own instance label covers target identity, so the exporter deliberately omits any server/host/target label. Error responses (404/503) are fixed strings that echo nothing from the request. The exporter binds loopback by default; binding a non-loopback address is allowed but warned, and exposing /metrics beyond a trusted network is the operator's responsibility (there is no authentication on the endpoint). HTTP hardening rejects non-GET methods, oversized headers, and unknown paths.

Mode interactions

  • --cluster-mode is supported. Counters are cluster-wide totals summed across all shard connections; the exporter binds its own TCP socket independently of the benchmark transport. (MOVED/ASK redirection counters are not yet exported.)
  • --verify-only performs no benchmark run, so the exporter serves only the constructor's zero snapshot (memtier_run 0, all counters 0, with memtier_exporter_snapshot_age_seconds growing) for the whole verification window. --verify-only requires --data-import (it implies --data-verify).
  • --unix-socket does not interact with the exporter: the benchmark talks to the target over the Unix socket while the exporter still binds its own TCP port.

Using with External StatsD/Graphite

If you have an existing StatsD-compatible metrics infrastructure:

./memtier_benchmark -s redis-server \
    --statsd-host=statsd.your-company.com \
    --statsd-port=8125 \
    --statsd-prefix=benchmarks.memtier \
    --statsd-run-label=redis-perf-$(date +%Y%m%d-%H%M%S)

Memtier Grafana dashboard for Graphite datasource

You can import the dashboard from grafana/dashboards/memtier.json into your Grafana instance. You may need to adjust the datasource UID to match your Graphite datasource.

Memtier Grafana dashboard for Prometheus datasource

You can import the dashboard from grafana/dashboards/prom-memtier.json into your Grafana instance.