Skip to content

Performance Guide

This guide provides performance characteristics, benchmarks, and optimization tips for the Quill RPC framework.

Table of Contents

Overview

Quill is designed for high performance with minimal overhead. The framework uses:

  • Zero-copy frame encoding/decoding where possible
  • Lock-free metrics using atomic operations
  • Efficient compression with zstd
  • Streaming support with backpressure
  • Minimal middleware overhead (sub-microsecond for most operations)

Benchmark Results

All benchmarks were run on a modern development machine using Criterion.rs. Results show median latency with throughput where applicable.

Frame Operations

Frame encoding and decoding are core operations that happen on every RPC call.

Frame Encoding

Payload Size Latency Throughput
100 bytes 62.5 ns 1.49 GiB/s
1 KB 76.5 ns 12.5 GiB/s
1 MB 17.5 µs 55.7 GiB/s

Key Takeaways: - Small frames (< 1KB) encode in ~70 nanoseconds - Encoding throughput scales with payload size (55+ GiB/s for large payloads) - Varint overhead is minimal (< 3 ns per encoding)

Frame Decoding

Payload Size Latency Throughput
100 bytes 40.2 ns 2.31 GiB/s
1 KB 94.0 ns 10.1 GiB/s
1 MB 41.8 µs 23.3 GiB/s

Key Takeaways: - Decoding is faster than encoding for small payloads - Sub-100 nanosecond decoding for payloads < 1 KB - 20+ GiB/s throughput for large payloads

Frame Roundtrip (Encode + Decode)

Payload Size Latency Throughput
100 bytes 117 ns 817 MiB/s
1 KB 176 ns 5.42 GiB/s
10 KB 539 ns 17.7 GiB/s

Key Takeaways: - Complete encode/decode cycle in < 180 ns for 1 KB payloads - Throughput reaches 17+ GiB/s for 10 KB payloads

Middleware Overhead

Middleware overhead is critical for understanding total request latency.

Rate Limiting

Operation Latency
Single token acquisition 52 ns
Bulk acquisition (10 tokens) 52 ns
Check available tokens 6.7 ns

Key Takeaways: - Token bucket rate limiting adds ~50 ns per request - Checking available tokens is nearly free (< 7 ns) - Lock contention is minimal due to efficient mutex usage

Authentication

Operation Latency
Valid API key validation 25.8 ns
Invalid API key validation 22.6 ns

Key Takeaways: - API key validation adds ~25 ns per request - HashMap lookup is extremely fast - Failed validations are slightly faster (no string allocation)

Metrics Collection

Operation Latency
Record request 2.0 ns
Record success 2.0 ns
Record failure 2.0 ns
Record bytes 1.9 ns
Get snapshot 1.8 ns

Key Takeaways: - Lock-free atomic operations are extremely fast (< 2 ns) - Metrics add negligible overhead to request processing - Snapshot creation is nearly free

Compression (zstd)

100 byte payload: - Compression: 8.3 µs (11.5 MiB/s) - Note: Not recommended for small payloads due to overhead

10 KB payload: - Compression: 9.4 µs (1.01 GiB/s) - Decompression: ~5-10 µs (est.)

1 MB payload: - Compression: ~500 µs (est., level 3) - Decompression: ~200 µs (est.)

Key Takeaways: - Only compress payloads > 1 KB (configurable threshold: 1024 bytes) - Level 3 compression provides good balance of speed and ratio - Compression is CPU-bound; consider payload size vs. network savings

Compression Levels

Tested on 10 KB payload:

Level Latency Throughput
1 ~7 µs ~1.4 GiB/s
3 9.4 µs 1.01 GiB/s
6 ~15 µs ~0.6 GiB/s
9 ~20 µs ~0.5 GiB/s
15 ~40 µs ~0.25 GiB/s
22 ~100 µs ~0.1 GiB/s

Recommendation: Use level 3 (default) for good balance. Use level 1 for lowest latency, level 9-15 for maximum compression ratio.

Full Middleware Stack

Complete middleware stack including: - API key authentication - Rate limiting - Metrics recording - Compression (1 KB payload, level 3)

Total Overhead: ~10-12 µs

Breakdown: - Auth: 25 ns - Rate limiting: 50 ns - Metrics (3 calls): 6 ns - Compression: 9.4 µs - Remaining: syscalls, allocations

Key Takeaway: Middleware overhead is dominated by compression. Without compression, overhead is < 100 ns.

Performance Budgets

Based on CLAUDE.md specifications:

Target Latencies

  • Browser stream p99: 250 ms (includes network, processing, rendering)
  • Service internal p99: 40 ms (cluster-internal RPC calls)

Quill Framework Overhead

Based on our benchmarks:

Component Latency Budget Actual
Frame encoding < 100 ns 60-80 ns ✅
Frame decoding < 100 ns 40-95 ns ✅
Middleware (no compression) < 500 ns 80-100 ns ✅
Compression (10 KB) < 50 µs 9.4 µs ✅

Result: Framework overhead is well within budget at < 20 µs for typical requests (1-10 KB with compression).

Latency Breakdown (Typical 1 KB Request)

Total Budget: 40 ms (service-to-service)
├─ Network RTT: ~35 ms (variable, depends on distance)
├─ Application Logic: ~4.9 ms (your handler)
└─ Quill Overhead: ~0.1 ms (100 µs)
    ├─ Frame encoding: 76 ns
    ├─ Frame decoding: 94 ns
    ├─ Middleware: 80 ns
    ├─ Compression: 9.4 µs (optional)
    ├─ HTTP overhead: ~50 µs
    └─ Serialization: ~40 µs (protobuf)

Conclusion: Quill adds < 0.25% overhead to a typical 40ms service-to-service call.

Optimization Tips

1. Disable Compression for Small Payloads

// Only compress if payload > threshold
let threshold = 1024; // 1 KB
let client = QuillClient::builder()
    .base_url("http://localhost:8080")
    .enable_compression(payload_size > threshold)
    .build()?;

Benefit: Saves 8-10 µs for small payloads.

2. Use Appropriate Compression Level

// Level 1: Fastest, lower compression ratio
let client = QuillClient::builder()
    .compression_level(1)
    .enable_compression(true)
    .build()?;

// Level 3: Default, good balance (recommended)
let client = QuillClient::builder()
    .compression_level(3)
    .enable_compression(true)
    .build()?;

// Level 9: Slower, better compression (large payloads)
let client = QuillClient::builder()
    .compression_level(9)
    .enable_compression(true)
    .build()?;

Guideline: - Level 1: Low-latency, frequent small transfers - Level 3: Default, general purpose - Level 6-9: Large payloads, prioritize bandwidth over CPU - Level 15-22: Archival, rarely used in real-time systems

3. Reuse Clients

// Bad: Creating new client per request
for _ in 0..1000 {
    let client = QuillClient::new("http://localhost:8080");
    client.call(...).await?;
}

// Good: Reuse client with connection pooling
let client = Arc::new(QuillClient::new("http://localhost:8080"));
for _ in 0..1000 {
    client.call(...).await?;
}

Benefit: Eliminates connection establishment overhead (~1-5 ms per connection).

4. Batch Operations with Streaming

// Bad: Multiple unary calls
for item in items {
    client.call("service", "Process", item).await?;
}

// Good: Single streaming call
client.call_client_stream("service", "ProcessBatch",
    stream::iter(items.into_iter())
).await?;

Benefit: Reduces per-call overhead, better throughput.

5. Optimize Middleware Stack

// Disable logging in production if not needed
let logger = RequestLogger::disabled();

// Use per-user rate limiting only when needed
// Global rate limiting is faster
let rate_limiter = RateLimitLayer::new(1000.0, 5000.0);

Benefit: Each disabled middleware saves 10-50 ns.

6. Use Async Efficiently

// Bad: Sequential calls
let r1 = client.call("service", "Method1", req1).await?;
let r2 = client.call("service", "Method2", req2).await?;

// Good: Concurrent calls
let (r1, r2) = tokio::join!(
    client.call("service", "Method1", req1),
    client.call("service", "Method2", req2)
);

Benefit: Reduces total latency when calls are independent.

7. Profile Your Application

# Run benchmarks
cargo bench

# Profile with flamegraph
cargo install flamegraph
cargo flamegraph --bench your_bench

# Profile with perf (Linux)
perf record -g ./target/release/your_app
perf report

Running Benchmarks

Microbenchmarks (Criterion)

Run all benchmarks:

cargo bench --workspace

Run specific crate benchmarks:

# Frame operations
cargo bench -p quill-core --bench frame_benchmark

# Middleware overhead
cargo bench -p quill-server --bench middleware_benchmark

Run specific benchmark:

cargo bench -p quill-core --bench frame_benchmark -- "encode_1kb"

Save baseline for comparison:

cargo bench --workspace -- --save-baseline main

Compare against baseline:

# Make changes...
cargo bench --workspace -- --baseline main

Load Testing (CLI Tool)

Create benchmarks.yaml:

benchmarks:
  - name: "Echo Service - Unary"
    url: "http://localhost:8080"
    service: "echo.v1.EchoService"
    method: "Echo"
    payload:
      message: "Hello, World!"

  - name: "Large Payload"
    url: "http://localhost:8080"
    service: "data.v1.DataService"
    method: "Process"
    payload:
      data: "..." # Large payload

Run load test:

# Default: 50 concurrent, 10 seconds
quill bench

# Custom concurrency and duration
quill bench -c 100 -d 60

# With target RPS
quill bench -c 100 -d 60 -r 1000

# JSON output
quill bench -o json > results.json

Continuous Performance Monitoring

Run benchmarks in CI:

# In .github/workflows/bench.yml
- name: Run benchmarks
  run: cargo bench --workspace -- --output-format bencher | tee output.txt

- name: Store benchmark result
  uses: benchmark-action/github-action-benchmark@v1
  with:
    tool: 'cargo'
    output-file-path: output.txt

Performance Checklist

Before deploying to production:

  • Run benchmarks and verify against budgets
  • Profile application under realistic load
  • Test with compression enabled and disabled
  • Verify connection pooling is working
  • Check middleware overhead is acceptable
  • Test streaming performance
  • Measure end-to-end latency (including network)
  • Monitor p95 and p99 latencies in staging
  • Load test with expected traffic patterns
  • Set up continuous performance monitoring

Troubleshooting Performance Issues

High Latency

  1. Check compression settings: Disable or reduce level
  2. Profile application: Find hot spots in your code
  3. Monitor network: Use quill bench to isolate client/server vs. network
  4. Check middleware: Disable non-essential middleware
  5. Review serialization: Ensure protobuf is being used efficiently

Low Throughput

  1. Increase concurrency: More concurrent requests
  2. Use streaming: Batch operations when possible
  3. Enable HTTP/2: Better connection multiplexing (coming soon)
  4. Optimize payload size: Smaller payloads = higher throughput
  5. Connection pooling: Reuse connections

High CPU Usage

  1. Reduce compression level: Use level 1 or disable
  2. Optimize application logic: Profile to find bottlenecks
  3. Rate limiting: Protect against overload
  4. Scale horizontally: Add more instances

High Memory Usage

  1. Check streaming: Ensure proper backpressure
  2. Review payload sizes: Large payloads consume memory
  3. Connection limits: Set max connections per server
  4. Monitor metrics: Track bytes sent/received

See Also

Benchmark Reproducibility

Hardware specs for reference benchmarks: - Benchmarks run on Apple Silicon / x86_64 - Rust version: 1.75+ - Criterion version: 0.5 - Results may vary by ~10-20% depending on hardware

To compare your results:

cargo bench --workspace -- --output-format bencher