spdlog Async Overflow Policies: Block vs Overrun Oldest Explained

When spdlog's asynchronous logging queue fills up, the block policy causes the calling thread to wait until space is available, while overrun_oldest immediately discards the oldest pending log entry to accept the new message without blocking.

The gabime/spdlog library implements high-performance asynchronous logging through a lock-free MPMC blocking queue that buffers log messages between producer threads and background worker threads. When this queue reaches capacity, the async_overflow_policy enum defined in include/spdlog/async_logger.h (lines 21-27) determines whether the system prioritizes data integrity or latency.

Understanding the Async Overflow Policy Enum

spdlog defines three overflow behaviors, though production code typically chooses between the two primary policies.

The Policy Definition

In include/spdlog/async_logger.h, the enum specifies the available strategies:

  • block: The calling thread blocks until the queue has space. The log message is always eventually enqueued.
  • overrun_oldest: The queue drops the oldest entry to make room for the new one. No blocking occurs.
  • discard_new: The new message is silently discarded when the queue is full (useful when older logs must be preserved).

The policy is stored as a member variable overflow_policy_ inside the async_logger class and defaults to block in the constructor.

Behavioral Comparison

Policy Latency Impact Data Guarantee Use Case
block Variable (waits for space) 100% delivery Financial transactions, audit logs
overrun_oldest Zero (immediate return) Recent data prioritized Real-time telemetry, high-frequency monitoring

How Overflow Policies Work Internally

The policy selection affects how the thread pool interacts with the underlying lock-free queue.

Thread Pool Dispatch Logic

When a log message is posted, include/spdlog/details/thread_pool-inl.h (lines 81-89) implements the policy-specific behavior in post_async_msg_:

if (overflow_policy == async_overflow_policy::block) {
    q_.enqueue(std::move(new_msg));               // blocks until space available
} else if (overflow_policy == async_overflow_policy::overrun_oldest) {
    q_.enqueue_nowait(std::move(new_msg));        // drops oldest if full
} else {
    q_.enqueue_if_have_room(std::move(new_msg));  // drops new if full
}
  • q_.enqueue performs a blocking push that waits for free space.
  • q_.enqueue_nowait internally calls pop_front on the queue when full, discarding the oldest element before inserting the new one.
  • q_.enqueue_if_have_room checks capacity and skips insertion if the queue is saturated.

Async Logger Integration

In include/spdlog/async_logger-inl.h, the sink_it_ method forwards messages to the thread pool using the stored policy:

// Forward to thread pool with the overflow policy
pool_ptr->post_log(shared_from_this(), msg, overflow_policy_);

This ensures that every log call respects the overflow policy configured at logger creation.

Implementing spdlog Async Overflow Policies

Configure the behavior by passing the async_overflow_policy enum to the async_logger constructor or using the factory aliases.

Example: Default Block Policy

By default, async loggers use the block policy to guarantee message delivery:

#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    // Create thread pool with 8192 message capacity
    auto tp = std::make_shared<spdlog::details::thread_pool>(8192, 1);
    
    // Default overflow_policy is block
    auto logger = std::make_shared<spdlog::async_logger>(
        "my_logger",
        spdlog::sinks_init_list{std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log")},
        tp);  // Defaults to async_overflow_policy::block

    logger->info("This call blocks if the queue is full, ensuring the message is logged.");
}

Example: Overrun Oldest Configuration

For high-throughput scenarios where latency is critical, explicitly select overrun_oldest:

#include <spdlog/async.h>
#include <spdlog/sinks/basic_file_sink.h>

int main() {
    auto tp = std::make_shared<spdlog::details::thread_pool>(8192, 1);
    
    auto logger = std::make_shared<spdlog::async_logger>(
        "my_logger",
        spdlog::sinks_init_list{std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log")},
        tp,
        spdlog::async_overflow_policy::overrun_oldest);  // Non-blocking

    logger->info("If the queue is full, the oldest entry is dropped to make room for this.");
}

Using Factory Aliases

spdlog provides convenience aliases that pre-select the policy:

using spdlog::async_factory;          // block policy (default)
using spdlog::async_factory_nonblock; // overrun_oldest policy

// Create logger using non-blocking factory
auto logger = async_factory_nonblock::create<spdlog::sinks::basic_file_sink_mt>(
    "app.log", /*queue_size=*/8192);

Choosing Between Block and Overrun Oldest

Select the appropriate policy based on your application's constraints:

  • Use block when log completeness is mandatory. This is essential for compliance logging, financial records, or debugging data where every message counts. The trade-off is potential latency spikes in the producer thread when the system is under heavy load.

  • Use overrun_oldest when you need guaranteed low latency and recent data is more valuable than stale logs. This suits real-time telemetry, performance metrics, or monitoring systems where a 30-second-old log is less relevant than the current error state.

Summary

  • spdlog async overflow policies control behavior when the MPMC queue reaches capacity, defined in include/spdlog/async_logger.h.
  • block (default) causes the producer thread to wait via q_.enqueue(), ensuring 100% message delivery at the cost of potential latency.
  • overrun_oldest calls q_.enqueue_nowait() to drop the oldest entry when full, maintaining non-blocking operation for the producer.
  • The policy is passed from async_logger to thread_pool via post_log and evaluated in post_async_msg_ in thread_pool-inl.h.
  • Factory aliases async_factory and async_factory_nonblock provide convenient configuration shortcuts.

Frequently Asked Questions

What is the default spdlog async overflow policy?

The default policy is block, defined in the async_logger constructor in include/spdlog/async_logger.h. If you do not specify an overflow policy when creating an async logger, the producer thread will wait for queue space rather than dropping messages.

Does overrun_oldest delete the oldest log entry permanently?

Yes. When using overrun_oldest and the queue is full, q_.enqueue_nowait() internally calls pop_front to remove the oldest entry before inserting the new message. That log entry is lost and will not be written to the sink.

Can I monitor how many messages spdlog drops?

spdlog does not expose a direct counter for dropped messages in the overrun_oldest or discard_new policies. If you need to track drops, you must implement a custom sink or wrap the queue logic, as the standard MPMC blocking queue implementation does not provide statistics on discarded items.

What is the difference between overrun_oldest and discard_new?

overrun_oldest removes the oldest message from the queue to make room for the new one, preserving the most recent log data. discard_new simply refuses to add the incoming message if the queue is full, preserving all older messages but losing the current one. Choose overrun_oldest for real-time systems where recent data matters, and discard_new when you must guarantee that once-queued messages are never evicted.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →