# Performance Tuning Adjust throughput, latency, and sampling to fit your workload. For indicative performance numbers, see [Benchmarks](benchmarks.md). For protecting latency under slow sinks, see [Non-blocking Async Logging](../cookbook/non-blocking-async-logging.md). ## Worker count (most impactful) The `worker_count` setting controls parallel flush processing and has the largest impact on throughput: | Configuration | Throughput | vs Default | |---------------|------------|------------| | 1 worker (default) | ~3,500/sec | baseline | | 2 workers | ~105,000/sec | **+30x faster** | | 2 workers + redaction | ~89,000/sec | +26x | **Recommendation:** Use 2 workers for production workloads. Production-oriented presets (`production`, `fastapi`, `serverless`, `hardened`) default to 2 workers automatically. ```python from fapilog import LoggerBuilder # Option 1: Use a production preset (recommended) logger = LoggerBuilder().with_preset("production").build() # Option 2: Explicitly set worker count logger = LoggerBuilder().with_workers(2).build() ``` ```bash # Via environment variable export FAPILOG_CORE__WORKER_COUNT=2 ``` **Why 2 workers is optimal:** - More than 2 workers shows diminishing returns due to asyncio scheduler overhead - Queue size barely matters - larger queues actually hurt slightly (memory overhead) - Workers are the bottleneck with `worker_count=1` (serializes all processing) **Note on "context switching":** Workers are asyncio tasks running in the dedicated background thread's event loop, not OS threads. There's no OS-level context switching between workers—they run cooperatively. The "overhead" with 3+ workers is the asyncio scheduler managing more tasks within the same thread. See [Execution Modes](execution-modes.md) for details. **When to use 1 worker:** - Development/debugging (simpler log ordering) - Dev preset uses 1 worker by default for this reason ## Queue and batch tuning ```bash # Throughput-friendly export FAPILOG_CORE__MAX_QUEUE_SIZE=20000 export FAPILOG_CORE__BATCH_MAX_SIZE=256 export FAPILOG_CORE__BATCH_TIMEOUT_SECONDS=0.25 # Latency-sensitive export FAPILOG_CORE__MAX_QUEUE_SIZE=5000 export FAPILOG_CORE__BATCH_MAX_SIZE=64 export FAPILOG_CORE__BATCH_TIMEOUT_SECONDS=0.1 export FAPILOG_CORE__DROP_ON_FULL=true export FAPILOG_CORE__BACKPRESSURE_WAIT_MS=10 ``` ## Protected levels (priority-aware dropping) Under queue pressure, fapilog uses priority-aware dropping to protect high-value events. By default, ERROR, CRITICAL, and FATAL events are protected from queue drops. ```python from fapilog import LoggerBuilder # Default: ERROR, CRITICAL, FATAL protected logger = LoggerBuilder().with_preset("production").build() # Custom: Also protect AUDIT and SECURITY levels logger = ( LoggerBuilder() .with_protected_levels(["ERROR", "CRITICAL", "FATAL", "AUDIT", "SECURITY"]) .build() ) # Disable priority dropping (FIFO behavior) logger = LoggerBuilder().with_protected_levels([]).build() ``` ```bash # Via environment variable export FAPILOG_CORE__PROTECTED_LEVELS='["ERROR", "CRITICAL", "FATAL", "AUDIT"]' ``` **How it works:** - When the queue is full and a protected-level event arrives, an unprotected event is evicted - Uses O(1) tombstone-based eviction (no queue scanning) - Maintains temporal ordering within each priority class - Falls back to normal drop behavior when no eviction candidates exist **Performance characteristics:** - Normal enqueue: O(1), no overhead vs standard queue - Eviction enqueue: O(1), just marks a tombstone - Dequeue: O(1) amortized, skips tombstones with lazy compaction **When to customize:** - Add custom levels (AUDIT, SECURITY) for compliance logging - Set `[]` to disable priority dropping for strict FIFO behavior - Leave default for most production workloads ## Adaptive pipeline (automatic tuning) Instead of manually tuning workers, batch sizes, and queue sizes, the adaptive pipeline monitors queue pressure and adjusts automatically. ```python from fapilog import LoggerBuilder # One-line setup with the adaptive preset logger = LoggerBuilder().with_preset("adaptive").build() # Or enable adaptive on any preset logger = ( LoggerBuilder() .with_preset("production") .with_adaptive(max_workers=8) .build() ) # Enable batch sizing when using batch-aware sinks (CloudWatch, Loki, PostgreSQL) logger = ( LoggerBuilder() .with_preset("production") .with_adaptive(max_workers=8, batch_sizing=True) .add_cloudwatch("/myapp/prod") .build() ) ``` ```bash # Via environment variables export FAPILOG_ADAPTIVE__ENABLED=true export FAPILOG_ADAPTIVE__MAX_WORKERS=8 export FAPILOG_ADAPTIVE__BATCH_SIZING=true ``` **What it adjusts:** | Actuator | Normal | Elevated | High | Critical | |----------|--------|----------|------|----------| | Workers | Initial (2) | +1 | +2 | Max (8) | | Batch size (opt-in) | Base (100) | 1.5x | 2x | 4x | | Queue capacity | Base | 1.5x | 2x | Up to 4x | | Filter tightening | None | Soft | Medium | Aggressive | **When to use adaptive vs manual:** - **Adaptive**: Variable workloads, microservices, deployments where traffic spikes are unpredictable - **Manual**: Steady-state services where you know exact throughput, latency-critical paths where you want deterministic behavior See [Adaptive Pipeline](adaptive-pipeline.md) for threshold configuration and detailed tuning. ## Sampling low-severity logs Use `observability.logging.sampling_rate` to drop a fraction of DEBUG/INFO logs: ```bash export FAPILOG_OBSERVABILITY__LOGGING__SAMPLING_RATE=0.2 # keep 20% of DEBUG/INFO ``` ## Serialization fast-path Enable `core.serialize_in_flush=true` when sinks support `write_serialized` to reduce per-entry serialization overhead in sinks. ## Metrics Enable internal metrics to monitor queue depth, drops, flush latency: ```bash export FAPILOG_CORE__ENABLE_METRICS=true ```