Performance Tuning
Felix trades between latency, throughput, and memory with explicit knobs. This page explains which knob moves which needle, gives three starting-point profiles, and points at the measured numbers. Nothing here has seen production — the configurations are benchmark-tested starting points, and your own measurement outranks all of them.
Understanding Felix Performance
Section titled “Understanding Felix Performance”Felix performance is determined by several interconnected factors:
- Network transport: QUIC connection and stream configuration
- Batching: Message aggregation at publish and delivery stages
- Parallelism: Connection pools and worker threads
- Buffering: Queue depths and flow control windows
- Encoding: Binary wire framing efficiency
- Outbound lanes: Subscriber writer lane count and lane sharding policy
Performance Profiles
Section titled “Performance Profiles”Felix provides three pre-configured profiles as starting points. They are the same pipeline with one dial turned — how long a message is allowed to wait for company before being sent:
flowchart LR
subgraph lat["Latency-optimized"]
direction TB
L1(["message"]) l1@--> L2["send immediately<br/><small>batch ≈ 1, shallow queues,<br/>block instead of drop</small>"]
L2 l2@--> L3(["lowest p99<br/><small>fewest messages per syscall</small>"])
end
subgraph bal["Balanced (default)"]
direction TB
B1(["message"]) b1@--> B2["brief coalescing window<br/><small>moderate batching + pools</small>"]
B2 b2@--> B3(["sub-ms latency at<br/>useful throughput"])
end
subgraph thr["Throughput-optimized"]
direction TB
T1(["message"]) t1@--> T2["fill the batch<br/><small>deep queues, large windows,<br/>lossless pacing</small>"]
T2 t2@--> T3(["most bytes per second<br/><small>latency includes batch fill</small>"])
end
l1@{ animation: fast }
l2@{ animation: fast }
b1@{ animate: true }
b2@{ animate: true }
t1@{ animation: slow }
t2@{ animation: slow }
classDef step fill:#e8f0fe,stroke:#4a6fa5,color:#1a2b40
classDef ok fill:#e9f5ec,stroke:#4a8a5e,color:#16301f
classDef warm fill:#fdf0e3,stroke:#b07d3a,color:#3d2a12
class L2,B2,T2 step
class L1,B1,T1 ok
class L3,B3 ok
class T3 warm
Balanced Profile (Default)
Section titled “Balanced Profile (Default)”General-purpose settings for mixed workloads:
Broker configuration:
# Connection poolspub_conn_pool: 4pub_streams_per_conn: 2event_conn_pool: 8cache_conn_pool: 8cache_streams_per_conn: 4
# QUIC flow controlevent_conn_recv_window: 268435456 # 256 MiBevent_stream_recv_window: 67108864 # 64 MiBevent_send_window: 268435456 # 256 MiB
# Batchingevent_batch_max_events: 64event_batch_max_bytes: 65536event_batch_max_delay_us: 250fanout_batch_size: 64
# Queue depths (defaults favor bounded latency + visible overload over deep buffering)pub_queue_depth: 64pub_inflight_bytes: 67108864 # 64 MiB shared in-flight publish byte budgetsubscriber_queue_capacity: 512subscriber_writer_lanes: 4subscriber_lane_queue_depth: 64max_subscriber_writer_lanes: 8subscriber_lane_shard: auto
publish_chunk_bytes: 16384Expected performance: see Benchmarks for current, measured
numbers across payload/fanout shapes — this profile is the default the
harness runs against. Numbers here are intentionally not duplicated to avoid
drift; the benchmarks page is regenerated from latency-demo and is the
source of truth.
Best for:
- Mixed pub/sub, cache and consumer-group workloads
- Moderate fanout (1-20 subscribers)
- General application development
- Starting point for tuning
Latency-Optimized Profile
Section titled “Latency-Optimized Profile”Minimize tail latency at the cost of throughput:
Broker configuration:
# Smaller poolspub_conn_pool: 2pub_streams_per_conn: 1event_conn_pool: 4
# Smaller windowsevent_conn_recv_window: 67108864 # 64 MiBevent_stream_recv_window: 16777216 # 16 MiBevent_send_window: 67108864 # 64 MiB
# Minimal batchingevent_batch_max_events: 8event_batch_max_bytes: 32768event_batch_max_delay_us: 100fanout_batch_size: 8
# Fast acknowledgementsack_on_commit: true
# Shallow queues, blocking backpressure instead of drops, single writer per# connection for stable per-message orderingpub_queue_depth: 32subscriber_queue_capacity: 64subscriber_queue_policy: blocksubscriber_writer_lanes: 2subscriber_lane_queue_depth: 32subscriber_lane_queue_policy: blocksubscriber_single_writer_per_conn: truesubscriber_flush_max_items: 1subscriber_flush_max_delay_us: 0subscriber_lane_shard: autoExpected performance: the latency-focused profile in Benchmarks (batch = 1, per-message acked) measures this shape directly — sub-millisecond p999 at fanout 1-10 on the reference hardware there.
Best for:
- Real-time interactive applications
- Trading systems, gaming
- Sensor data with immediate processing
- Low fanout (1-5 subscribers)
Throughput-Optimized Profile
Section titled “Throughput-Optimized Profile”Maximize throughput and burst tolerance:
Broker configuration:
# Large poolspub_conn_pool: 8pub_streams_per_conn: 4event_conn_pool: 16cache_conn_pool: 16cache_streams_per_conn: 8
# Large windowsevent_conn_recv_window: 536870912 # 512 MiBevent_stream_recv_window: 134217728 # 128 MiBevent_send_window: 536870912 # 512 MiB
# Aggressive batchingevent_batch_max_events: 256event_batch_max_bytes: 1048576event_batch_max_delay_us: 2000fanout_batch_size: 256
# Async acknowledgementsack_on_commit: false
# Deep queues, lossless end-to-end backpressure (paces the publisher to the# pipeline's sustainable rate instead of shedding), thread-per-core stream# ownership for multi-stream workloadspub_queue_depth: 256pub_inflight_bytes: 268435456 # 256 MiBpub_ingress_wait: truesubscriber_queue_capacity: 4096subscriber_queue_policy: blocksubscriber_writer_lanes: 8subscriber_lane_queue_depth: 1024subscriber_lane_queue_policy: blockmax_subscriber_writer_lanes: 8subscriber_lane_shard: autocore_shards: 4 # tune to (physical cores - 2); 0 = off
publish_chunk_bytes: 32768Expected performance: the throughput-focused profile in
Benchmarks (batch = 64, lossless, zero drops)
measures this shape directly. Message rate falls and byte rate rises as
payloads grow, so read the byte rate when comparing payload sizes.
core_shards may help multi-stream workloads, but its published gains predate
the transport scheduling work and need re-validation.
Best for:
- High-throughput data pipelines
- Log aggregation, metrics collection
- High fanout (20-100+ subscribers)
- Batch processing workflows
Configuration Parameters
Section titled “Configuration Parameters”Pub/Sub Parameters
Section titled “Pub/Sub Parameters”Connection Pooling
Section titled “Connection Pooling”event_conn_pool: 8 # QUIC connections for eventspub_conn_pool: 4 # QUIC connections for publishingpub_streams_per_conn: 2 # Publish streams per connectionTuning guidance:
| Workload | event_conn_pool | pub_conn_pool | streams_per_conn |
|---|---|---|---|
| Light | 2-4 | 2 | 1-2 |
| Medium | 4-8 | 2-4 | 2 |
| Heavy | 8-16 | 4-8 | 2-4 |
| Very heavy | 16-32 | 8-16 | 4-8 |
Flow Control Windows
Section titled “Flow Control Windows”event_conn_recv_window: 268435456 # Per-connection receive windowevent_stream_recv_window: 67108864 # Per-stream receive windowevent_send_window: 268435456 # Per-connection send windowMemory impact calculation:
Worst-case memory = (conn_window × conn_pool) + (stream_window × avg_streams × conn_pool)Example:
conn_pool=8,conn_window=256MB,stream_window=64MB,avg_streams=10- Memory ≈ (256MB × 8) + (64MB × 10 × 8) = 2GB + 5.1GB = 7.1GB
Tuning guidance:
- Low latency, limited bursts: Use smaller windows (64-128 MiB)
- High throughput, bursty: Use larger windows (256-512 MiB)
- Memory constrained: Reduce pool size before reducing windows
Batching Parameters
Section titled “Batching Parameters”event_batch_max_events: 64 # Max events per batchevent_batch_max_bytes: 262144 # Max batch size (256 KB)event_batch_max_delay_us: 250 # Max batching delay (250 µs)fanout_batch_size: 64 # Fanout batch sizeBatch triggers: Event batch is sent when any condition is met.
Trade-off analysis:
| Parameter | ↑ Increase Effect | ↓ Decrease Effect |
|---|---|---|
max_events |
Higher throughput, higher latency | Lower latency, lower throughput |
max_delay_us |
Higher throughput, higher latency | Lower latency, lower throughput |
max_bytes |
Fewer frames, more efficiency | More frames, less efficiency |
fanout_batch_size |
Better fanout efficiency | Lower fanout latency |
Recommended settings by workload:
| Workload | event_batch_max_events |
event_batch_max_delay_us |
|---|---|---|
| Ultra-low latency | 4 | 50 |
| Low latency | 8 | 100 |
| Balanced (default) | 64 | 250 |
| High throughput | 128 | 1000 |
| Maximum throughput | 256 | 2000 |
For example, the high-throughput profile as a config file:
event_batch_max_events: 128event_batch_max_delay_us: 1000Queue Depths and Byte Budgets
Section titled “Queue Depths and Byte Budgets”pub_queue_depth: 64 # Publish pipeline queue (items)pub_inflight_bytes: 67108864 # Shared in-flight publish byte budget (bytes, not items)subscriber_queue_capacity: 512 # Per-subscriber broker-core queuesubscriber_lane_queue_depth: 64 # Per-lane outbound writer queuepub_workers_per_conn: 4 # Publish workers per connection (ignored when core_shards > 0)Design intent: defaults are deliberately shallow. pub_queue_depth and
the lane queues bound how much can queue before backpressure or shedding
kicks in — the goal is throughput that plateaus with bounded latency and
overload that’s visible (drops, counters), not a deep buffer that hides
backlog until it OOMs or the tail latency becomes unbounded. pub_inflight_bytes
is a second, independent budget on bytes rather than item count, so a few
large batches can’t blow past the ingress memory budget even with a small
pub_queue_depth.
- Shallower (production default direction): lower memory, backpressure/drops surface sooner, bounded tail latency.
- Deeper (opt-in, throughput profile): higher burst tolerance and memory, and only safe paired with
subscriber_queue_policy: block+pub_ingress_wait: true(lossless pacing) — otherwise deep queues just delay when drops happen, not whether they happen.
Memory per queue:
Queue memory ≈ queue_depth × avg_message_size
Example (default subscriber_queue_capacity=512): 512 × 4KB = 2MB per subscriber queueWith 100 subscribers: 100 × 2MB = 200MBOutbound Writer Lanes and Sharding
Section titled “Outbound Writer Lanes and Sharding”Writer lanes parallelize outbound subscriber writes while preserving per-subscriber ordering.
subscriber_writer_lanes: 4max_subscriber_writer_lanes: 8subscriber_lane_queue_depth: 64subscriber_lane_shard: auto # auto | subscriber_id_hash | connection_id_hash | round_robin_pinStart here:
subscriber_lane_shard: autosubscriber_writer_lanes: 4- Increase to
8only if throughput is still lane-bound - Avoid assuming larger lane counts always help; watch p99/p999
- For multi-stream workloads, also evaluate
core_shards(thread-per-core stream ownership) — see Benchmarks, which showed larger gains there than lane count alone.
Cache Parameters
Section titled “Cache Parameters”cache_conn_pool: 8 # QUIC connections for cachecache_streams_per_conn: 4 # Streams per connectioncache_conn_recv_window: 268435456 # 256 MiB per connectioncache_stream_recv_window: 67108864 # 64 MiB per streamConcurrency calculation:
Max concurrent cache ops = cache_conn_pool × cache_streams_per_connRecommended by workload:
| Workload | conn_pool | streams_per_conn | Max Concurrency |
|---|---|---|---|
| Low | 4 | 2 | 8 |
| Medium | 8 | 4 | 32 |
| High | 16 | 8 | 128 |
| Very high | 32 | 16 | 512 |
Event Frame Encoding
Section titled “Event Frame Encoding”Subscription event delivery uses binary EventBatch framing by default.
Benchmark Results
Section titled “Benchmark Results”Pub/Sub Latency and Throughput
Section titled “Pub/Sub Latency and Throughput”See Benchmarks for current, methodology-documented
results: latency and throughput profiles across payload sizes and fanout,
the transport levers behind them (MTU/GSO, congestion window, socket
buffers), the core_shards thread-per-core lever, and how to regenerate the
numbers yourself with latency-demo. That page is generated from the same
harness referenced throughout this guide and is kept current; numbers are
intentionally not duplicated here to avoid the two pages drifting apart.
Cache Performance (Localhost)
Section titled “Cache Performance (Localhost)”Configuration: 8 connections, 4 streams/conn, concurrency=32
| Operation | Payload | p50 | p99 | Throughput |
|---|---|---|---|---|
| put | 0 B | 158 µs | 350 µs | 184k ops/sec |
| put | 256 B | 179 µs | 380 µs | 155k ops/sec |
| put | 4 KB | 260 µs | 480 µs | 78k ops/sec |
| get (hit) | 256 B | 177 µs | 360 µs | 166k ops/sec |
| get (miss) | - | 165 µs | 340 µs | 179k ops/sec |
Profiling and Diagnostics
Section titled “Profiling and Diagnostics”Telemetry Feature
Section titled “Telemetry Feature”Enable detailed performance telemetry:
[dependencies]felix-client = { version = "0.1", features = ["telemetry"] }felix-broker = { version = "0.1", features = ["telemetry"] }# Broker configdisable_timings: false # Enable timing measurementsMetrics collected:
- Per-operation latency histograms (publish, subscribe, cache)
- Frame counters (publish frames, event frames, cache frames)
- Queue depth samples
- Flow control events
Overhead: 5-15% throughput reduction in high-load scenarios.
Performance Debugging
Section titled “Performance Debugging”High publish latency:
- Check
pub_queue_depthandpub_inflight_bytes- is the queue or byte budget filling up? - Check
pub_workers_per_conn(orcore_shardsif enabled) - enough parallelism? - Check broker CPU usage - saturated?
- Enable telemetry - where is time spent?
- Check
felix_broker_ingress_dropped_total/felix_broker_ingress_rejected_total- is ingress shedding underpub_ingress_wait: false?
High subscribe latency:
- Check
subscriber_queue_capacity,subscriber_queue_policy, and lane drop counters - subscribers falling behind? - Check
event_batch_max_delay_us- batching too aggressive? - Check QUIC flow control - windows exhausted?
- Check subscriber processing time - bottleneck in application?
- Check path MTU discovery (
FELIX_MTU_UPPER_BOUND) - see Benchmarks for why this matters more than it looks.
Low throughput:
- Increase
event_batch_max_events- more aggressive batching - Increase connection pools - more parallelism
- Confirm binary
EventBatchdecoding path in subscribers - Check network bandwidth - saturated?
- Increase
pub_workers_per_conn- more publish parallelism
High memory usage:
- Reduce flow control windows
- Reduce queue depths
- Reduce connection pool sizes
- Check for slow subscribers - filling buffers?
Production Recommendations
Section titled “Production Recommendations”Sizing Guidelines
Section titled “Sizing Guidelines”Small deployment:
pub_conn_pool: 2event_conn_pool: 4cache_conn_pool: 4event_batch_max_events: 32pub_queue_depth: 32subscriber_queue_capacity: 64subscriber_writer_lanes: 2Expected resources: 2 CPU cores, 2-4 GB RAM
Medium deployment:
pub_conn_pool: 4event_conn_pool: 8cache_conn_pool: 8event_batch_max_events: 64pub_queue_depth: 64subscriber_queue_capacity: 512subscriber_writer_lanes: 4Expected resources: 4-8 CPU cores, 4-8 GB RAM
Large deployment (multi-stream, high fanout):
pub_conn_pool: 8event_conn_pool: 16cache_conn_pool: 16event_batch_max_events: 128pub_queue_depth: 256pub_inflight_bytes: 268435456subscriber_queue_capacity: 4096subscriber_writer_lanes: 8core_shards: 4 # tune to (physical cores - 2)Expected resources: 16-32 CPU cores, 16-32 GB RAM
Tuning Workflow
Section titled “Tuning Workflow”- Start with balanced profile: Use defaults
- Measure baseline: Run realistic workload, measure latency/throughput
- Identify bottleneck: CPU? Memory? Network? Queue depths?
- Tune one parameter: Change single parameter
- Re-measure: Verify improvement
- Iterate: Repeat until requirements met
Monitoring in Production
Section titled “Monitoring in Production”Key metrics to track:
- Publish rate and latency (p50, p99, p999)
- Subscribe rate and latency
- Queue depths (publish, event)
- Lane queue pressure (per-lane enqueue/drop/highwater)
- Connection count
- CPU and memory usage
- Network bandwidth
- Dropped event count
- Slow subscriber count
Alert against your own measured baseline (say, p99 above twice it) rather than absolute numbers — the useful thresholds are workload-shaped.
Hardware Recommendations
Section titled “Hardware Recommendations”- Minimum: 2 cores
- Recommended: 4-8 cores for medium workloads
- High performance: 16-32 cores for high throughput
Felix is CPU-bound for:
- QUIC encryption (TLS 1.3 AEAD, always on)
- Wire encoding/decoding — binary by default for unacknowledged publishes
and always for event delivery; JSON only for acked publishes and explicit
publish_json/publish_batch_jsoncalls - Fanout: encoding happens once per publish batch and the encoded frame is
shared across subscribers (not re-encoded per subscriber), so this scales
with publish rate rather than
publish rate × fanout
Memory
Section titled “Memory”- Minimum: 2 GB
- Recommended: 4-8 GB for medium workloads
- High performance: 16-32 GB for high throughput with large queues
Memory usage scales with:
- Connection pool sizes × flow control windows
- Queue depths × subscriber count
- Cache size
Network
Section titled “Network”- Minimum: 1 Gbps
- Recommended: 10 Gbps for high throughput
- Ideal: 25+ Gbps for very high throughput
QUIC benefits from:
- Low latency networks (< 1 ms RTT)
- High bandwidth
- Low packet loss (< 0.1%)
- Ephemeral streams: not used at all — no disk I/O on the hot path
- Durable streams: NVMe SSD strongly recommended. Under
fsync_mode = on_commiteach commit costs one device flush (~4ms on a typical NVMe), which group commit amortises across concurrent publishers; underperiodicthe flush is off the append path entirely. Measured figures and a regression budget are in storage-performance.md.
The whole method in one paragraph
Section titled “The whole method in one paragraph”Start from the balanced profile, run a realistic workload, and change one knob at a time off a measurement. Queue depths tell you where pressure is; batching buys throughput at the price of per-message latency; pools buy isolation at the price of memory. Leave telemetry off in production, keep 2–3× headroom above expected load, and write down what you changed and what it measured — the next person tuning this will be you, six months out.
