Non-blocking logging in FastAPI (protect latency under slow sinks)

Slow log sinks can stall your async application. When a network hiccup delays CloudWatch or your disk fills up, synchronous logging blocks the event loop, affecting every concurrent request. fapilog’s async pipeline with configurable backpressure lets you choose: drop logs to protect latency, or block to guarantee delivery.

Note: Whether you use get_logger() in sync code or get_async_logger() in async code, your log calls never block on I/O. The non-blocking benefits described here apply to both APIs.

The Problem: Slow Sinks Block Your App

In a traditional logging setup, each log call writes directly to the destination:

Request → log.info("...") → [WAIT for network/disk] → Continue processing

When the sink is fast, this works fine. But sinks can slow down:

  • Network latency: CloudWatch API taking 500ms instead of 50ms

  • Disk I/O: Log rotation or full disk causing writes to stall

  • External services: Loki or Elasticsearch under heavy load

In an async framework like FastAPI, a blocking log call doesn’t just slow one request—it blocks the entire event loop:

Request A → log.info() → [BLOCKED 500ms waiting for CloudWatch]
Request B → waiting...
Request C → waiting...
Request D → waiting...

A single slow log sink can turn your 10ms API into a 500ms+ API.

The Solution: Async Pipeline with Backpressure

fapilog decouples log emission from sink delivery:

Request → log.info() → [Queue] → Worker → Sink
              ↓
         Returns immediately

Log calls return immediately after enqueueing. A background worker handles delivery, isolating your request handlers from sink latency.

But what happens when logs arrive faster than the sink can process them? The queue fills up. fapilog provides two backpressure modes to handle this:

Mode

Behavior

Use When

Drop (default)

Wait briefly, then drop the log

Latency is critical

Block

Wait indefinitely for queue space

Every log must be delivered

Configuring Backpressure

Drop Mode (Latency-Critical Services)

Fapilog uses a dedicated background thread for its logging pipeline. The only work on your caller thread is try_enqueue() — a non-blocking put that takes microseconds:

from fapilog import get_async_logger, Settings

settings = Settings()
settings.core.drop_on_full = True  # Drop logs if queue is full (default)

logger = await get_async_logger(settings=settings)

With the dedicated thread architecture, log.info() will:

  1. Try to enqueue immediately (non-blocking)

  2. If the queue is full, drop the log and return instantly

Your request handler never blocks on logging.

Tuning Queue Size

The queue acts as a buffer between log emission and sink delivery. Size it to absorb traffic spikes:

settings.core.max_queue_size = 50_000  # Default: 10,000

A larger queue absorbs longer bursts but uses more memory. A smaller queue drops sooner under load.

Audit-Critical Services

For services where every log must be delivered (financial transactions, security events), use a large queue and protected levels:

settings.core.max_queue_size = 100_000  # Large buffer
settings.core.protected_levels = ["ERROR", "CRITICAL", "AUDIT", "SECURITY"]

Protected levels use priority eviction — when the queue is full, lower-priority events are evicted to make room for protected ones.

Default behavior: fapilog defaults to drop mode (drop_on_full=True) with non-blocking enqueue. This protects latency out of the box. Size your queue to match your burst traffic profile.

Monitoring Backpressure

fapilog exposes metrics to track queue health in production:

Queue Depth

The queue_depth_high_watermark in logger stats shows the maximum queue depth reached:

stats = await logger.get_stats()
print(f"Queue high watermark: {stats.queue_depth_high_watermark}")

If this approaches max_queue_size, you’re hitting backpressure regularly.

Dropped Events

When using Prometheus metrics (enable_metrics=True), fapilog exports:

fapilog_events_dropped_total

A rising counter indicates logs are being dropped due to backpressure. This is expected in drop mode during sink slowdowns, but sustained drops may indicate:

  • Queue size too small for your throughput

  • Sink consistently slower than log emission rate

  • Need to scale sink capacity or reduce log volume

Example: FastAPI with Protected Latency

from fastapi import FastAPI, Depends
from fapilog.fastapi import FastAPIBuilder, get_request_logger

# Configure for latency-critical API with backpressure
app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .with_backpressure(drop_on_full=True, wait_ms=50)  # Max 50ms wait
        .build()
)

@app.get("/api/orders/{order_id}")
async def get_order(order_id: str, logger=Depends(get_request_logger)):
    # This log call returns in <50ms even if CloudWatch is slow
    await logger.info("Fetching order", order_id=order_id)
    return {"order_id": order_id}

Going Deeper