Why moving logging to another thread doesn’t make it cheaper — and where the cost really goes

Async logging is often treated as an obvious optimization.

It isn’t.

It just moves the cost somewhere else.

This idea sounds simple: synchronous logging blocks, async logging doesn’t — so it must be faster.

But once you look at what actually happens inside the system, the picture becomes very different.

Libraries like Quill are built around asynchronous pipelines. Others, like spdlog, support both synchronous and asynchronous modes. Some systems — including logme — deliberately mix synchronous formatting with asynchronous output.

Despite these differences, they all run into the same fundamental constraints.

Async logging does not eliminate the cost of logging. It simply moves it around.

The work is still there: formatting, copying, synchronization, I/O. The only question is where it is paid — and when.

This is not a critique of asynchronous logging as a concept. Async logging is a powerful tool — when used for the right reasons. It can improve responsiveness and isolate slow outputs from critical paths. The problem is not async itself, but the assumption that it automatically makes logging cheaper. It doesn’t — it only redistributes the cost.

It’s Not One Thing

What is usually called “async logging” is actually two very different mechanisms bundled together.

One part is about how data is prepared. The other is about how it is delivered.

This distinction becomes obvious if you look at real implementations. In Quill logging library, a significant part of the system is dedicated to safely transferring arguments across threads. In synchronous configurations of spdlog, formatting and output stay on the caller thread, and the problem simply disappears.

You can move output to another thread.
You cannot avoid preparing the data.

Deferred Formatting Is Not Free

A common idea is to delay formatting entirely. Instead of producing a string on the caller thread, the system stores a format string and arguments, and lets a backend thread handle formatting later.

That approach is attractive and widely used. But it only works under one condition: the data must still be valid when the backend processes it.

And this is where things start to break.

std::string s = MakeText();
std::string_view sv = s;
LOG_INFO("{}", sv);
s.clear();

At the call site, everything looks fine. But by the time the backend thread sees the data, sv may already be invalid.

This is not an edge case. It is a fundamental limitation of deferred processing.

What Systems Actually Do

Once you accept the lifetime problem, the set of possible solutions becomes surprisingly small.

You either copy data at the call site, or you format it immediately, or you introduce custom serialization.

Different libraries make different trade-offs here, but none of them escape this triangle.

In practice, this means that “deferred formatting” almost always includes an eager capture step — copying or serializing data before it is enqueued.

At that point, the pipeline is no longer as simple as it seems.

The Pipeline Is Longer Than It Looks

Async logging is often imagined as:

enqueue → format → output

In reality, it looks like this:

capture → enqueue → format → output

That capture step is not optional. It is required for correctness.

And once you include it, the advantage of deferred formatting becomes less obvious. In many cases, copying plus deferred formatting ends up being comparable to just formatting immediately.

The work is still there.

A Note on Real Measurements

This is not just theoretical.

If you look at publicly available benchmarks comparing spdlog, Quill, and other libraries (see this write-up: What Does LOG_INFO() Really Cost? A Benchmark of C++ Logging Libraries), one pattern shows up repeatedly:

formatting often dominates the cost of logging

This becomes especially visible in synchronous configurations, where formatting happens directly on the caller thread and its cost cannot be hidden behind queues or background workers.

Once formatting becomes expensive enough, moving it to another thread does not make it cheaper. It only changes where the cost appears.

This also explains why heavily optimized synchronous formatting paths can compete with — or even outperform — naive async pipelines in real benchmarks.

Async Output Doesn’t Increase Throughput

Moving output to a separate thread is useful. It decouples the caller from I/O latency and reduces stalls.

But it does not make I/O faster.

If your backend can process 100k messages per second and your producers generate 1 million, the difference accumulates. No library changes that — whether it is Quill logging library or async mode in spdlog.

Queues do not remove work. They delay when you notice the problem.

The Single-Threaded Bottleneck

Many async logging systems rely on a single backend thread.

This simplifies design, but it also creates a hard limit. All messages eventually pass through one consumer.

Even worse, the cost of sinks is cumulative. Writing to multiple outputs increases the total work per message. A slow sink slows everything down.

This is a structural property, not an implementation detail.

Overload Doesn’t Disappear

When producers generate more data than the backend can process, something has to give.

Buffers grow. Producers block. Messages get dropped.

Async logging does not eliminate overload. It defines how overload behaves.

An Alternative Perspective

At this point, a different approach becomes more interesting.

Instead of trying to defer formatting, you can optimize it aggressively and keep it on the caller thread — where data is still valid and cheap to access.

Then move only the output part to asynchronous backends.

This avoids lifetime issues, reduces the need for copying, and keeps the system simpler. It is also a pattern that shows up in real systems, including logme, where formatting is treated as a hot-path operation and optimized accordingly, while I/O is handled asynchronously.

The important point is not the specific library, but the idea:

async output without async formatting is often a more predictable design

Final Thought

Async logging is often presented as a way to make logging cheap.

It isn’t.

It is a way to decide where the cost is paid — on the producer thread, on the backend thread, in memory, or in lost messages.

Once you look at it this way, the design space becomes much clearer.