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; acks=1, batch.size=16384, linger.ms=5

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 korvet_broker_produce_seconds histogram

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:

  1. Creates a logs-tiered topic with remote.storage.enable=true, small segments (4 MB TIERED_SEGMENT_BYTES), and short local retention (60s local.retention.ms)

  2. Preloads a fixed backlog (5M records full overlay / 100k small overlay) using the same record size and compression as the produce ramp

  3. 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.retention deletes the offloaded local copies

  4. Ramps Spark consumer replicas through the same steps as the local consume ramp (TIERED_CONSUME_REPLICA_STEPS)

  5. 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_seconds histogram for responses carrying remote-tier records, total request latency from korvet_broker_fetch_seconds, and client-observed latency from the latency probe

  • Results location: pipeline/consume-tiered/step-<replicas> and pipeline/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

TIERED_CONSUME_REPLICA_STEPS

Spark consumer replica counts to ramp through (e.g., "1 2 4 6" for full overlay, "1 2" for small)

TIERED_CONSUME_BACKLOG_MESSAGES

Number of records to preload into the tiered topic (5M full, 100k small)

TIERED_SEGMENT_BYTES

Segment roll threshold (4 MB) — small segments create many sealed segments eligible for offload

TIERED_LOCAL_RETENTION_MS

How long sealed segments stay local after offload (60s) before trim deletes them

TIERED_OFFLOAD_TIMEOUT_SECONDS

Maximum wait for backlog to reach S3 (3600s full, 1800s small)

TIERED_FETCH_P99_MS_LIMIT

Fetch delivery p99 saturation threshold for S3 reads (5000 ms)

TIERED_WORKER_CONCURRENCY

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-test from 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

Korvet Resource Usage

Metric Value

Process CPU

2.35%

System CPU

20.51%

JVM Memory Used

244.13 MB

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:

  1. Start Korvet with the specified Redis pool size

  2. Create a topic with 16 partitions

  3. Run 8 concurrent producers, each sending 1,000,000 messages

  4. Collect metrics from Korvet (via actuator) and Redis Enterprise (via API)

  5. 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-test from 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

Single Producer (Baseline)

Metric Value

Throughput

151,860 records/sec

Throughput (MB/sec)

148.30 MB/sec

Total Messages

200,000

Average Latency

160 ms

P99 Latency

329 ms

8 Concurrent Producers

Metric Value

Aggregate Throughput

168,641 records/sec

Throughput (MB/sec)

164.68 MB/sec

Total Messages

674,564

Duration

4.58 seconds

Average Latency (range)

495-724 ms

Memory Used

776.47 MB

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%

Storage Efficiency

Metric Value

Messages archived

4,000,000

S3 objects created

400 (one Parquet per sealed segment)

Total S3 storage

~8 MB

Bytes per message

~2 bytes (after SNAPPY compression)

Compression ratio

~50:1

Archival Configuration

The storage worker was configured with:

korvet:
  storage:
    remote:
      path: s3://your-bucket/korvet
      s3:
        region: us-west-1
    worker:
      enabled: true

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-test from 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 (batch.size=131072)

Linger

5ms (linger.ms=5)

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

Sizing Recommendations for Redis Flex

Workload Shards RAM per Shard

Light (< 50k rec/s)

4

2.5GB

Medium (50-100k rec/s)

8

1.25GB+

Heavy (100k+ rec/s)

8-16

1GB+ (with throttling)

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-delay appropriately (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

  1. Starts Korvet with the specified Redis pool size

  2. Flushes Redis to ensure clean state

  3. Creates topic with specified number of partitions

  4. Runs producers using kafka-producer-perf-test

  5. 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

  6. 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