Async vs sync trade-offs
Async logging is not strictly better than sync — it trades tail latency and complexity for throughput, and it changes what "the log survived the crash" actually means. Reach for it because you've measured a problem it solves, not by default.
Comparison table
| Sync logger | Async logger | |
|---|---|---|
| Caller-thread cost | Format + write, on the caller | Format + queue copy, on the caller |
| Throughput | Bounded by sink I/O speed | Bounded by queue drain rate |
| Message ordering | Always call order | Guaranteed only with one worker thread |
| Memory | Minimal | Queue size × per-message cost |
| Crash durability | Whatever's flushed survives | Anything still queued is lost |
| Shutdown complexity | Trivial | Must drain and join workers |
| Debuggability | Log line appears exactly when logged | Log line can lag behind real time |
When async wins
High message rates where the caller thread can't afford to wait on I/O; slow sinks (network destinations, syslog under load, a rotating file sink on a busy disk); latency-sensitive callers where even a few hundred microseconds of synchronous file I/O per request adds up.
When async loses
Low log volume where sync I/O was never the bottleneck; short-lived processes where the thread pool's own startup/shutdown overhead outweighs anything it saves; anything where a message lost on crash is unacceptable; tests that assert on log output synchronously and don't want to wait for or coordinate with a background worker.
The crash case
A sync logger with flush_on(err) writes the last words before a crash to disk before the process
actually dies. An async logger's queued messages are still in memory when abort()/segfault happens
— they never reach the sink. If "what led up to the crash" matters, keep a sync logger (or a sync
error-level path) for exactly that purpose. See
Flush policies and
Backtrace and crash dump for
complementary approaches to the same problem.
The middle ground
A common pattern: keep console/error output on a synchronous logger (low volume, and you want it immediately) while routing high-volume diagnostic logging through an async logger. Two loggers, not one — see Multi-sink loggers if you want to combine destinations within a single sync or single async logger instead.
Measuring before switching
With compile-time level filtering in place, most disabled log calls already cost effectively nothing — see Compile-time log level. Profile before assuming async is the fix; often the actual cost is enabled logs at a level you didn't need to enable in production.
See also
- Thread pool and async logger — the mechanism behind the async column above.
- Overflow policies — what happens when async can't keep up.
- Compile-time log level — the cheaper fix to check first.
- spdlog overview.