|
This version is still in development and is not considered stable yet. For the latest stable version, please use Korvet 0.19! |
Benchmarks
This page documents performance benchmarks for Korvet with Redis Enterprise as the storage backend.
If you are sizing a deployment, start with the pipeline saturation benchmark: it runs daily on a production-grade Kubernetes topology with dedicated node groups per component, ramps load until a component saturates, and reports which component is the bottleneck. The other benchmarks on this page were captured on developer or single-host setups and are best read as component-level characterizations.
Benchmark CLI
The installed korvet executable includes producer, consumer, and coordinated round-trip workloads.
They use the Apache Kafka Java client and can target Korvet or any Kafka-compatible broker without a
Kafka distribution or source checkout.
With Korvet listening on the default localhost:9092, run the coordinated producer and consumer
benchmark with:
korvet bench
This creates a unique korvet-benchmark-<uuid> topic and leaves it in place so the run can be
inspected. Pass --cleanup to delete it after the result is written. benchmark remains the long
form of bench, and benchmark roundtrip remains available for compatibility.
# Producer throughput and send-to-ack latency
korvet benchmark producer \
--bootstrap-server localhost:9092 \
--topic benchmark \
--create-topic --partitions 16 \
--clients 4 --records 5000000 \
--record-size 1KB --rate unlimited
# Consume existing benchmark records with checksum validation
korvet benchmark consumer \
--bootstrap-server localhost:9092 \
--topic benchmark \
--clients 4 --records 5000000 \
--from beginning --validate checksum
# Coordinate both sides and fail if acknowledged records are missing
korvet benchmark roundtrip \
--bootstrap-server localhost:9092 \
--topic benchmark-roundtrip \
--create-topic --partitions 16 \
--producer-clients 4 --consumer-clients 4 \
--records 5000000 --record-size 1KB --rate 100000
The producer reports aggregate records/s, MiB/s, buffered records, errors, and an HDR distribution
of send-to-ack latency. roundtrip embeds a versioned envelope in each value, measures end-to-end
latency, validates the run identity, and reports matched, duplicate, invalid, and missing records.
It exits nonzero when production, consumption, or correctness checks fail.
Byte-size options accept unitless bytes or case-insensitive B, KB, MB, and GB suffixes.
For example, --fetch-max-bytes 1mb is equivalent to --fetch-max-bytes 1048576.
Duration options use Spring Boot duration syntax. Unitless values are milliseconds; supported
case-insensitive suffixes are ns, us, ms, s, m, h, and d. ISO-8601 values such as
PT30S are also accepted.
Use --output json for one machine-readable final result, --output jsonl for interval records plus
the final result, and --result-file <path> to write the final JSON atomically. Pass
--metrics-port 9400 to expose /metrics and /health while a workload runs.
TLS, SASL, and other standard Kafka client settings can be loaded from a properties file:
korvet benchmark producer \
--bootstrap-server broker.example.com:9092 \
--command-config client.properties \
--topic benchmark
The CLI never flushes Redis or truncates a topic. A default round-trip run creates a unique topic;
--create-topic creates a named topic only when it is missing. Topics remain available for
inspection unless --cleanup is explicitly passed to a round-trip run. Use an isolated topic or
reset an isolated environment outside the CLI when repeatable storage state is required.
Pipeline Saturation Benchmark (EKS)
This benchmark measures the peak sustainable throughput of a realistic production pipeline: Logstash producers and Spark consumers interacting over the Kafka protocol with a multi-broker Korvet deployment on EKS, backed by a Redis Enterprise cluster using the OSS Cluster API. It runs daily against the latest build and includes three ramps:
-
Produce ramp — Logstash producers sending JSON events (documented below)
-
Consume ramp — Spark consumers reading from a preloaded backlog in Redis (local storage)
-
Tiered consume ramp — Spark consumers reading from a preloaded backlog archived to S3 (remote storage)
Each trend record carries a workflow run ID that is independent of the Git commit, so consecutive scheduled runs remain distinct even when the source commit and runtime image are unchanged. Trend reports identify the current and baseline invocations by run ID, record timestamps, commit, and runtime image digest.
The results below are from the 2026-08-11 run.
Test Environment
| Component | Configuration |
|---|---|
Korvet brokers |
3 replicas (StatefulSet, one broker per node), 6 vCPU / 12 GB each, 6 GB heap, on m5.2xlarge nodes |
Redis Enterprise |
3 nodes (r6i.2xlarge, 7 vCPU / 52 GB per pod), 24 master shards, 120 GB database, OSS Cluster API, sparse shard placement |
Topic |
96 partitions (4× the shard count), LZ4 compression, Redis-only retention |
Producers |
Logstash with the Kafka output plugin, 1 pod per c5.xlarge node, ramped 1 → 16 replicas; |
Events |
~700-byte JSON log events, LZ4-compressed on the wire |
Each ramp step measures a 120-second window after a 60-second warm-up, collecting broker metrics (Prometheus actuator), Redis Enterprise per-shard/per-node CPU (REST API), and producer rates. The benchmark also runs a dedicated latency probe — a single pod that produces and consumes timestamped records at a fixed low rate (100 msg/s in full runs, 50 msg/s in small runs) against its own single-partition topic — to measure client-observed latency alongside the broker’s server-side request histograms:
-
Produce-ack latency: Time from scheduled send to broker acknowledgment, including client queueing, batching, and network round-trip
-
End-to-end latency: Time from scheduled send to record consumed by a continuously-polling consumer
The probe uses coordinated-omission-safe scheduling (latencies measured from scheduled send time, not actual send time), so client stalls under broker pushback appear in the measurements rather than hiding between samples. Because the probe’s producer and consumer run in the same process, clock skew is not a factor.
A step is saturated when any component crosses its limit (CPU ≥ 90%, produce p99 > 250 ms, backpressure or connection-pool timeouts) or throughput plateaus.
Performance Results
Peak throughput: 642,352 msg/s (46.5 MB/s compressed) at 16 producers — bottleneck: Redis Enterprise shard CPU.
| Producers | msg/s | MB/s | p50 (ms) | p99 (ms) | ack p99 (ms) | e2e p99 (ms) | Korvet CPU avg | RE max shard CPU |
|---|---|---|---|---|---|---|---|---|
1 |
74,506 |
5.5 |
2.1 |
5.4 |
- |
- |
8% |
4% |
2 |
150,043 |
11.1 |
2.3 |
6.4 |
- |
- |
16% |
8% |
4 |
286,401 |
21.3 |
2.8 |
9.1 |
- |
- |
32% |
13% |
6 |
409,528 |
30.3 |
3.6 |
13.8 |
- |
- |
48% |
23% |
9 |
528,756 |
39.1 |
5.5 |
24.7 |
- |
- |
65% |
24% |
12 |
597,657 |
43.9 |
7.7 |
37.6 |
- |
- |
74% |
28% |
16 |
642,352 |
46.5 |
10.1 |
59.0 |
- |
- |
79% |
92% |
The p50/p99 columns show broker-side request handling latency (from the korvet_broker_produce_seconds histogram). The ack p99 and e2e p99 columns show client-observed latency from the paced probe. These measurements will be included in future benchmark runs.
|
Key Findings
-
Throughput scales near-linearly with load until saturation: 74.5k msg/s per producer at step 1, still 40k msg/s per producer at the 642k peak
-
Produce latency stays low well past half load: p99 under 25 ms up to ~530k msg/s; latency only climbs as the hottest shard approaches saturation
-
The bottleneck at peak is Redis Enterprise shard CPU (hottest shard at 92%), not the brokers (79% average) — adding shards and nodes raises the ceiling
-
A single broker ceilings at ~200k msg/s on internal contention regardless of CPU; 3 brokers measured ~2.6× a single broker, so scale brokers horizontally rather than vertically
-
Partitions must significantly exceed shard count: stream keys land on shards by slot hash, so equal counts leave some shards double-loaded and others empty; 4× (96 partitions on 24 shards) evens placement and spreads partition leadership across brokers
Latency Measurement Methodology
The benchmark reports two types of latency measurements that serve different purposes:
| Metric | What It Measures | Use Case |
|---|---|---|
Broker-side latency (p50/p99) |
Server-side request processing time from the |
Broker performance tuning, detecting broker-side bottlenecks |
Client-observed ack latency (ack p99) |
Total time from scheduled send to broker acknowledgment, including client queueing, batching, linger time, and network round-trip |
End-user experience, SLA validation, detecting network or client-side issues |
Client-observed e2e latency (e2e p99) |
Total time from scheduled send to record consumed by a continuously-polling consumer |
End-to-end pipeline latency, detecting consumer lag or replication delays |
The client-observed metrics are captured by a dedicated latency probe pod that runs throughout each benchmark ramp. The probe produces and consumes timestamped records at a fixed rate (100 msg/s for full runs, 50 msg/s for small runs) against its own single-partition latency-probe topic. Key characteristics:
-
Coordinated-omission-safe: Latencies are measured from each record’s scheduled send time on a fixed-interval schedule. If the client cannot send on schedule due to broker pushback, the delay appears in the latency measurement rather than being hidden.
-
Minimal overhead: The probe’s traffic is negligible compared to the ramp’s load (typically <0.02% of peak throughput), so it measures latency at the ramp’s load level without contributing to it.
-
Single-process measurement: Both the producer and consumer run in the same process using the same monotonic clock, eliminating clock skew concerns.
-
Producer settings match the load generators: The probe uses the same Kafka client configuration as the Logstash producers (
acks=1,linger.ms=5, same compression codec).
The probe’s cumulative latency histograms are snapshotted at each measurement window boundary and diffed to calculate per-step percentiles, using the same methodology as the broker-side histograms.
Consume summaries use probe e2e p99 as the client-visible latency headline.
Broker-side fetch delivery p99 comes from korvet_broker_fetch_delivery_seconds
and includes only responses that carry records; it is the fetch latency used
for saturation decisions. Total request p99 remains visible from
korvet_broker_fetch_seconds and includes intentional fetch.max.wait.ms
long-poll waits. korvet_broker_fetch_empty_total tracks successful responses
that deliver no records, preserving visibility into empty long-poll frequency.
Deployment Sizing Guidance
Use the measured topology as a unit of capacity and scale each component by the reported bottleneck:
| Target sustained ingest | Korvet brokers | Redis Enterprise |
|---|---|---|
Up to ~150k msg/s |
1 broker (6 vCPU, 12 GB) |
6+ shards across 3 nodes |
Up to ~400k msg/s |
2–3 brokers |
12+ shards across 3 nodes |
Up to ~600k msg/s |
3 brokers |
24 shards across 3 nodes (the measured configuration) |
Beyond 600k msg/s |
+1 broker per ~200k msg/s |
Add shards and nodes; with the OSS Cluster API, throughput scales roughly linearly with shard count |
Additional sizing rules from this benchmark:
-
Partitions: provision at least 4× the shard count, and enough to spread leadership across all brokers.
-
Redis memory: size the database for
peak rate × retention window × average message size × replication factor, plus headroom for shard skew and trim-tick overshoot (the storage worker trims retention in ticks, so the working set carries up to one tick interval of extra data at high ingest). At 582k msg/s with 60 s retention and ~700-byte events, peak measured usage was ~51 GB. -
Producer batching: keep Kafka batch sizes small (16 KB); broker request service time grows superlinearly with request size — 256 KB batches measured ~2.4× slower than 16 KB.
-
Headroom: treat these numbers as a floor for the measured hardware; leave margin for consumer traffic, rebalances, and failover.
These figures cover produce-side ingest with Redis-only retention. Tiered (S3/Iceberg) offload sustains roughly an order of magnitude less throughput than saturation ingest — see the remote storage archival benchmark — so size retention and offload capacity separately if you enable remote storage.
Tiered Storage Consume Benchmark
The daily pipeline benchmark also measures consume throughput from tiered storage (S3/Iceberg remote tier). This ramp runs after the local consume ramp and characterizes the fetch path when serving archived data from object storage.
How It Works
The tiered consume ramp:
-
Creates a
logs-tieredtopic withremote.storage.enable=true, small segments (4 MBTIERED_SEGMENT_BYTES), and short local retention (60slocal.retention.ms) -
Preloads a fixed backlog (5M records full overlay / 100k small overlay) using the same record size and compression as the produce ramp
-
Measures and waits for S3 offload to complete: diffs the metrics baseline captured before preload against the stable zero-lag snapshot, then waits one additional retention tick so
local.retentiondeletes the offloaded local copies -
Ramps Spark consumer replicas through the same steps as the local consume ramp (
TIERED_CONSUME_REPLICA_STEPS) -
Remote-read verification: after every measured window, diffs
korvet_storage_read_messages_sum{tier="remote"}and aborts if the window served no remote reads
Only each partition’s open tail segment stays in Redis, so full-topic scans are remote-dominated — the same architecture the integration tests assert via LOCAL_DELETED state.
Key Characteristics
-
Fetch latency threshold: uses
TIERED_FETCH_P99_MS_LIMIT=5000(5 seconds) instead of the local-tier default (500 ms), since S3 reads carry Parquet row group overhead and object-store round trips -
Metrics collected: per-step throughput, broker
korvet_broker_fetch_delivery_secondshistogram for responses carrying remote-tier records, total request latency fromkorvet_broker_fetch_seconds, and client-observed latency from the latency probe -
Results location:
pipeline/consume-tiered/step-<replicas>andpipeline/consume-tiered/saturation -
Offload result:
pipeline/offload, with archived bytes/s and segments/s, drain wall-clock duration, archive p50/p99, failures/retries when available, and maximum observed archive lag. Archived byte throughput is the trend regression headline. -
Shared infrastructure: retargets the same Spark consumer deployment used by the local consume ramp (restored on exit)
The Spark driver publishes progress only when a complete full-topic batch
scan finishes. Since a remote scan can span a measurement window, a zero or
reduced Spark counter delta does not establish load-generator backpressure. The
tiered attribution uses broker fetch counters together with successful S3 GET
rate: if S3 operations stop scaling as consumers are added while fetch delivery
p99 rises, the result is a korvet bounded remote-read concurrency bottleneck.
|
Treat that verdict as remote-tier broker capacity. Scaling broker replicas is
the first tuning direction when partition placement permits it. Increasing the
remote Iceberg read-thread count or changing max.poll.records or
max.partition.fetch.bytes changes the benchmark tuning contract; record such a
run as a new trend baseline instead of comparing it directly with the existing
series.
Manual workflow runs can set these controls through the existing
producerTuning input. It accepts space-separated key=value entries:
icebergReadThreads, icebergReadQueueCapacity, icebergOffloadThreads,
workerTickConcurrency, maxPollRecords, maxPartitionFetchBytes,
rowGroupBytes, and tieredSteps. The tiered step value is comma-separated
and each entry must be one of 1,2,4,6,8,10. Existing batchBytes,
bufferBytes, and loadgenNodes keys remain available; brokerStack selects
netty or reactor-netty without consuming another dispatch input.
The workflow rejects malformed, unsupported, zero, and negative values before creating infrastructure. With no tuning input, scheduled and manual runs keep the overlay defaults. Every scenario records the effective values and summary reports print the canonical tuning string; a tuning change is marked as an incompatible baseline rather than reported as a performance delta.
Configuration
The tiered consume ramp uses these environment variables (from benchmark/eks/env/<overlay>.env):
| Variable | Description |
|---|---|
|
Spark consumer replica counts to ramp through (e.g., |
|
Number of records to preload into the tiered topic (5M full, 100k small) |
|
Segment roll threshold (4 MB) — small segments create many sealed segments eligible for offload |
|
How long sealed segments stay local after offload (60s) before trim deletes them |
|
Maximum wait for backlog to reach S3 (3600s full, 1800s small) |
|
Fetch delivery p99 saturation threshold for S3 reads (5000 ms) |
|
Storage worker tick concurrency recorded with the offload scenario (8, matching the runtime default) |
Running the Benchmark
The tiered consume ramp is part of the daily .github/workflows/pipeline-benchmark.yml workflow:
# The workflow runs all three ramps in sequence:
# 1. Produce ramp (run-benchmark.sh)
# 2. Local consume ramp (run-consume-benchmark.sh)
# 3. Tiered consume ramp (run-tiered-consume-benchmark.sh)
# To run manually on an existing EKS cluster:
cd benchmark/eks
source env/small.env # or full.env
./scripts/run-tiered-consume-benchmark.sh
Results are emitted to build/pipeline-benchmark/tiered-consume-summary.md and
build/pipeline-benchmark/offload-summary.md. Both the tiered consume records
and the pipeline/offload record are appended to the trend series on the
pipeline-benchmarks branch. Completeness validation requires the current
successful tiered ramp to have a valid offload record; completed earlier ramp
results are still persisted before a later failure marks the workflow red.
The nightly full overlay uses the validated saturation configuration: eight
Iceberg read threads, max.poll.records=2000,
max.partition.fetch.bytes=4194304, and the 1,2,4,6,8,10 tiered ramp. It
retains the default 128 MiB Parquet row group and four offload threads. This
drives Korvet to its broker CPU ceiling and records maximum sustainable
throughput in the existing results.json series, so subsequent nightly runs
detect regressions against the same tuned configuration. Individual workflow
tuning keys remain available for manual experiments.
Performance Considerations
-
S3 throughput is significantly lower than local storage — expect roughly an order of magnitude less throughput than Redis-only fetch saturation
-
Latency is higher and more variable — healthy S3 fetch delivery p99 sits in the 1-5 second range vs. sub-second local fetches
-
Consumer timeout configuration: when consuming from tiered storage in production, increase consumer timeouts to account for S3 latency (see Remote Storage Archival Benchmark for recommended settings)
Optimal Configuration Benchmark
This benchmark demonstrates the best throughput configuration for Korvet with Redis Enterprise.
CI benchmarks exercise both the netty (default) and reactor-netty transport stacks. The results shown here use the default netty stack. reactor-netty stack benchmarks are run nightly for client-facing workloads (Produce, Consume, GroupConsume) to track transport-layer performance across both pipelines.
|
Test Environment
-
Redis Enterprise: 16 shards, running locally
-
Korvet: Single instance (macOS, Apple Silicon)
-
Kafka Tools:
kafka-producer-perf-testfrom Apache Kafka -
Topic Configuration: 16 partitions (1× the number of shards)
-
Record Size: 1 KB (1024 bytes)
-
Total Messages: 8,000,000 (1,000,000 per producer)
Configuration
| Parameter | Value |
|---|---|
Producers |
8 |
Batch Size |
1000 messages (1.07 MB) |
Redis Connection Pool Size |
8 |
Acks |
1 |
Compression |
none |
Linger |
0ms |
Performance Results
| Metric | Value |
|---|---|
Aggregate Throughput |
380,952 records/sec |
Throughput (MB/sec) |
372.02 MB/sec |
Total Messages |
8,000,000 |
Duration |
21 seconds |
Average Latency (range) |
249-411 ms |
95th Percentile Latency (range) |
657-2136 ms |
Redis Enterprise Metrics
| Metric | Value |
|---|---|
Total CPU (all 16 shards) |
79% |
Per-Shard CPU |
2-8% |
Data per Shard |
112-172 MB |
Data Distribution |
Even across all shards |
Key Findings
-
High throughput with low CPU usage: Achieved 372 MB/sec with only 2.35% Korvet CPU usage
-
Excellent scalability headroom: Both Korvet and Redis Enterprise operating well below capacity
-
Even load distribution: Data and CPU load distributed evenly across all 16 Redis shards
-
Optimal batch size: 1000 messages per batch provided the best balance of throughput and latency
Running This Benchmark
To reproduce this benchmark, use the provided benchmark script from the korvet-dist repository:
git clone https://github.com/redis-field-engineering/korvet-dist.git
cd korvet-dist/samples/benchmark/scripts
./run-comprehensive-benchmark.sh
The script will:
-
Start Korvet with the specified Redis pool size
-
Create a topic with 16 partitions
-
Run 8 concurrent producers, each sending 1,000,000 messages
-
Collect metrics from Korvet (via actuator) and Redis Enterprise (via API)
-
Generate a detailed report with throughput, latency, and resource usage
Results are saved to /tmp/korvet-benchmark-<timestamp>/.
Single Shard Benchmark
This benchmark demonstrates Korvet performance with a single Redis shard, providing a baseline for comparison with multi-shard configurations.
Test Environment
-
Redis Enterprise: 1 shard (~1 GB maxmemory), running locally
-
Korvet: Single instance (macOS, Apple Silicon)
-
Kafka Tools:
kafka-producer-perf-testfrom Apache Kafka -
Topic Configuration: 1 partition (matching the single shard)
-
Record Size: 1 KB (1024 bytes)
Configuration
| Parameter | Value |
|---|---|
Producers |
1 (baseline) / 8 (concurrent) |
Batch Size |
1000 messages (1.07 MB) |
Redis Connection Pool Size |
16 |
Acks |
1 |
Compression |
none |
Linger |
0ms |
Performance Results
Comparison: 16 Shards vs 1 Shard
| Metric | 16 Shards | 1 Shard | Ratio |
|---|---|---|---|
Database Memory |
~16 GB |
~1 GB |
16× |
Topic Partitions |
16 |
1 |
16× |
Throughput (rec/s) |
380,952 |
168,641 |
2.26× |
Throughput (MB/s) |
372.02 |
164.68 |
2.26× |
Per-shard throughput |
23,809 |
168,641 |
0.14× |
Key Findings
-
Single shard achieves ~44% of 16-shard aggregate throughput: 168,641 vs 380,952 records/sec
-
Higher per-shard efficiency with fewer shards: A single shard processes 168,641 rec/s vs 23,809 rec/s per shard in the 16-shard setup
-
Memory efficiency: ~1.15 KB per message in Redis Streams (776 MB for 674,564 messages)
-
Single producer baseline: 151,860 rec/s provides a clean baseline without concurrency overhead
Remote Storage Archival Benchmark
This benchmark measures the throughput of archiving sealed Redis stream segments to Apache Iceberg tables on S3.
Test Environment
-
EC2 Instance: c5.2xlarge (8 vCPU, 16GB RAM) in us-west-1
-
S3 Bucket: Same region (us-west-1) for optimal network performance
-
Redis: Docker container on same instance
-
Message Size: ~100 bytes (binary payload)
-
Compression: Iceberg default Parquet compression
Single Stream Results
Archiving from a single Redis Stream to S3:
| Messages | Archive Time | Throughput | Parquet Files |
|---|---|---|---|
1,000,000 |
31.3s |
31,970 msg/s |
100 @ 186ms avg |
Multi-Stream Results (4 Partitions)
Archiving from 4 Redis Streams in parallel to S3:
| Messages | Archive Time | Throughput | Parquet Files |
|---|---|---|---|
1,000,000 |
12.5s |
80,239 msg/s |
100 @ 212ms avg |
4,000,000 |
34.7s |
115,347 msg/s |
400 @ 192ms avg |
Scaling Summary
| Configuration | Throughput | vs Single Stream |
|---|---|---|
1 stream |
32k msg/s |
baseline |
4 streams (1M messages) |
80k msg/s |
2.5× |
4 streams (4M messages) |
115k msg/s |
3.6× |
Key Findings
-
Single stream peaks at ~32k msg/s: Bottleneck is S3 PUT latency for Parquet files
-
Near-linear scaling with streams: 4 streams achieves 115k msg/s (3.6× single stream)
-
Parquet writes average ~190ms: Same-region S3 provides consistent low latency
-
Excellent compression: SNAPPY on this payload achieves ~50:1 compression ratio (~2 bytes/message stored)
-
Same-region S3 is critical: Cross-region throughput drops ~50%
Redis Flex (Auto-Tiering) Benchmark
This benchmark evaluates Korvet performance with Redis Flex (Auto-Tiering), which uses NVMe flash storage to extend Redis capacity beyond RAM.
Test Environment
-
Redis Enterprise: 1× i4i.xlarge (4 vCPU, 32GB RAM, 937GB NVMe)
-
Database Config: 100GB capacity, 10GB RAM (10% ratio), 8 shards
-
Korvet Client: c7i.4xlarge (16 vCPU, 32GB RAM)
-
Kafka Tools:
kafka-producer-perf-testfrom Apache Kafka -
Record Size: 1 KB (1024 bytes)
-
Region: us-west-2 (all instances in same VPC)
Test Configuration
| Parameter | Value |
|---|---|
Instance Type (Redis) |
i4i.xlarge (NVMe-backed) |
Instance Type (Client) |
c7i.4xlarge |
Shards |
8 (1.25GB RAM per shard) |
Redis Pool Size |
256 |
Producer Batch Size |
128KB ( |
Linger |
5ms ( |
Acks |
1 |
Performance Results
| Metric | Korvet → Redis Flex | Direct Redis (XADD) |
|---|---|---|
Peak Throughput |
150,784 rec/s (147 MB/s) |
130,690 rec/s (128 MB/s) |
Sustained Throughput |
110,000 rec/s (107 MB/s) |
103,000 rec/s (100 MB/s) |
Average Latency |
201 ms |
< 1 ms |
P99 Latency |
510 ms |
28 ms |
Data Structure Comparison
We compared Redis Streams (XADD) vs simple key-value (SET) operations on Redis Flex:
| Operation | Throughput | Notes |
|---|---|---|
SET (1KB values) |
153,000 ops/sec |
Simple key-value, flash-friendly |
XADD (Streams, 1KB payload) |
103,000 ops/sec |
Stream data structure overhead |
Korvet → XADD |
110-150k rec/sec |
Near-native XADD performance |
Key Findings
-
Korvet matches native Redis Streams performance: Korvet achieved 110-150k rec/sec, matching or exceeding direct XADD benchmarks
-
Flash eviction is the bottleneck for sustained writes: RAM fills faster than NVMe can drain at very high throughput
-
Larger RAM buffers help: 8 shards (1.25GB RAM/shard) outperformed 48 shards (208MB RAM/shard) by avoiding OOM errors
-
Client instance sizing matters: Upgraded from t3.medium (2 vCPU) to c7i.4xlarge (16 vCPU) to eliminate client-side bottleneck
OOM Behavior
At sustained throughput above ~150k rec/sec with 1KB payloads, Redis Flex may return OOM errors when the RAM buffer fills faster than flash eviction can drain. This is inherent to Redis Streams on flash storage, not specific to Korvet.
| Scenario | Throughput | Result |
|---|---|---|
Burst (1M records) |
150k rec/s |
✅ Success |
Sustained (2M+ records) |
150k rec/s |
⚠️ OOM after ~1.3M records |
Sustained (unlimited) |
110k rec/s |
✅ Success |
Mitigation: For sustained high-throughput workloads on Redis Flex:
-
Use fewer shards with larger RAM buffers (e.g., 8 shards vs 48)
-
Increase RAM-to-disk ratio (e.g., 15-20% instead of 10%)
-
Throttle producer throughput to ~100k rec/sec per instance
-
Use multiple Redis Flex clusters for horizontal scaling
Configuration Recommendations
For optimal throughput:
-
Batch size: Use 1000 messages per batch for best balance of throughput and latency
-
Producers: 8 concurrent producers provides excellent throughput with manageable latency
-
Redis pool size: Match pool size to number of producers (8) for optimal connection utilization
-
Partitions: Use 1-2× the number of Redis shards (16 partitions for 16 shards)
-
Redis shards: Match the number of shards to available CPU cores
-
Rebalance delay: Configure
korvet.broker.rebalance-delayappropriately (default 3s) to allow all consumers to join before rebalancing -
Replication: Disable replication for write-heavy workloads (if durability requirements allow); conversely, for workloads that need Kafka
acks=all-like guarantees, see the replication-acknowledged produce option in the durability model
Running Your Own Benchmarks
Using the Benchmark Script
The korvet-dist repository contains a script to run benchmarks with various configurations.
git clone https://github.com/redis-field-engineering/korvet-dist.git
cd korvet-dist/samples/benchmark/scripts
./run-comprehensive-benchmark.sh
Configuration Options
Edit the script to customize benchmark parameters:
# Test parameters
TOPIC="benchmark-test"
PARTITIONS=16
RECORD_SIZE=1024
NUM_RECORDS=1000000
# Parameter arrays
PRODUCERS=(8) # Number of concurrent producers
BATCH_SIZES=(1000) # Messages per batch
POOL_SIZES=(8) # Redis connection pool size
What the Script Does
-
Starts Korvet with the specified Redis pool size
-
Flushes Redis to ensure clean state
-
Creates topic with specified number of partitions
-
Runs producers using
kafka-producer-perf-test -
Collects metrics:
-
Korvet CPU and memory (via Spring Boot Actuator at port 8080)
-
Redis Enterprise CPU and memory (via REST API at port 9443)
-
Producer throughput and latency
-
-
Generates report with detailed results
Output
Results are saved to /tmp/korvet-benchmark-<timestamp>/:
-
SUMMARY.txt: Summary table of all test results -
producers-<N>_batch-<B>msg_pool-<P>.txt: Detailed results for each test
Example summary output:
Producers Batch(msg) Pool Total Msgs Duration(s) Throughput(rec/s) Throughput(MB/s) 8 1000 8 8000000 21 380952 372.02
Running Storage Tier Benchmarks
Korvet ships JUnit-based micro-benchmarks alongside its integration tests. These
run under the Gradle integrationTest task (not test) and use Testcontainers,
so they require a running Docker engine.
Tiered read coordinator benchmark
RedisTieredGroupReadCoordinatorBenchmark in the korvet-storage-tiered-redis
module measures group-read coordination throughput across a range of batch sizes
against a Redis container. It is gated behind the korvet.benchmark.pel system
property so it is skipped during normal test runs:
./gradlew :korvet-storage-tiered-redis:integrationTest \
--tests "RedisTieredGroupReadCoordinatorBenchmark" \
-Dkorvet.benchmark.pel=true
Segment-scale benchmark
SegmentScaleBenchmarkTest in the korvet-benchmark module measures performance
characteristics at worst-case segment counts. A partition holding a full 25 GB
Redis Enterprise shard at the default 128 MB segment.bytes reaches approximately
200 sealed segments. This benchmark deterministically builds a 200-segment manifest
(one sealed segment per storage worker tick) and records trend scenarios for:
-
Produce send-to-ack latency: End-to-end produce performance with a large segment manifest
-
ListOffsets bounds resolution: Time to resolve earliest and latest offsets across 200 segments
-
Consume-from-earliest throughput: Standalone consumer reading through the entire partition
Each build round writes 96 records of 1 KiB per partition — comfortably above the
test topic’s 64 KiB segment.bytes budget — then waits for the storage worker tick
to seal the over-budget segment, so each round yields exactly one sealed segment
per partition. Results are keyed by segment count
(e.g., …/s200) for trend tracking across releases.
This pillar is included in the nightly benchmark run and skipped in baseline/reactor mode like other worker-dependent benchmarks. To run it manually:
./gradlew :korvet-benchmark:benchmark \
--tests "*.e2e.SegmentScaleBenchmarkTest" \
-Dkorvet.benchmark.segmentsPerPartition=200
The segment count is configurable via korvet.benchmark.segmentsPerPartition
(default 200). Lower values (e.g., 50) complete faster for smoke testing; higher
values stress-test metadata and offset resolution at extreme scale.
The remote/S3 archival and end-to-end remote-read numbers reported above
were produced with purpose-built harnesses on EC2. The reusable benchmark
scaffolding for capturing results — BenchmarkConfig, ScenarioResult, and
BenchmarkResult — lives in korvet-server/src/integrationTest under
com.redis.korvet.benchmark. A packaged, repeatable S3/remote-read benchmark
entry point is planned; the throughput figures in this page should be treated as
illustrative of what the storage tier can achieve rather than as a turnkey test
you can run as-is.
|
Result scaffolding
The BenchmarkResult type captures version, Git commit, and environment metadata
alongside per-scenario ScenarioResult entries, which makes results
comparable across releases. A captured result is shaped like this:
{
"korvetVersion": "0.5.0-ea1",
"timestamp": "2026-03-30T17:30:00Z",
"gitCommit": "abc1234",
"environment": {
"javaVersion": "25",
"osName": "Linux",
"availableProcessors": 4,
"maxMemoryMb": 4096
},
"config": {
"messageCount": 10000,
"recordSizeBytes": 1024,
"partitions": 1,
"iterations": 3,
"batchSizes": [100, 500, 1000]
},
"scenarios": [
{
"type": "STANDALONE_CONSUMER",
"batchSize": 100,
"avgLatencyMs": 250,
"p95LatencyMs": 320,
"throughputMsgPerSec": 400.0
}
]
}
Interpreting Results
-
Standalone vs Consumer Group overhead: Consumer group reads include additional coordination (JoinGroup, SyncGroup, OffsetFetch) which adds latency
-
ListOffsets Earliest latency: High values indicate slow remote-tier metadata lookups (Parquet footer reads against the manifest’s oldest REMOTE segment)
-
Throughput scaling: If throughput doesn’t scale linearly with batch size, there may be per-request overhead dominating
-
P95 vs Avg: Large gaps indicate object-store tail latencies or GC pauses
Timeout Recommendations
Based on production experience with S3-backed remote storage, configure consumer timeouts appropriately:
// For remote storage reads, increase timeouts
props.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, "60000"); // 60s
props.put(ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG, "120000"); // 2min
props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, "30000"); // 30s