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:
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:
Save baseline for comparison:
Compare against baseline:
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¶
- Check compression settings: Disable or reduce level
- Profile application: Find hot spots in your code
- Monitor network: Use
quill benchto isolate client/server vs. network - Check middleware: Disable non-essential middleware
- Review serialization: Ensure protobuf is being used efficiently
Low Throughput¶
- Increase concurrency: More concurrent requests
- Use streaming: Batch operations when possible
- Enable HTTP/2: Better connection multiplexing (coming soon)
- Optimize payload size: Smaller payloads = higher throughput
- Connection pooling: Reuse connections
High CPU Usage¶
- Reduce compression level: Use level 1 or disable
- Optimize application logic: Profile to find bottlenecks
- Rate limiting: Protect against overload
- Scale horizontally: Add more instances
High Memory Usage¶
- Check streaming: Ensure proper backpressure
- Review payload sizes: Large payloads consume memory
- Connection limits: Set max connections per server
- Monitor metrics: Track bytes sent/received
See Also¶
- Middleware Guide - Detailed middleware documentation
- Flow Control - Streaming backpressure
- Compression - Compression configuration
- Architecture - System design
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: