Lifecycle Considerations When Shutting Down Async spdlog Loggers: A Complete Guide

Call spdlog::shutdown() before program exit to drain message queues, stop the periodic flusher, and join background threads, preventing lost logs and undefined behavior.

The gabime/spdlog library provides high-performance asynchronous logging through background thread pools, but proper cleanup is essential to avoid resource leaks and ensure all messages reach their destinations. Understanding the lifecycle considerations when shutting down async spdlog loggers prevents the "static deinitialization order fiasco" and guarantees that queued messages are flushed before termination.

Why Async Logger Shutdown Matters

Asynchronous loggers in spdlog operate on a thread-pool that pulls messages from a lock-free queue and forwards them to sinks. Because logging work happens on background threads, the library must receive explicit notification to clean up resources before the program terminates. Without proper shutdown, queued messages may remain unwritten, background threads may outlive static objects, and the periodic flusher may attempt to access destroyed resources.

Core Components in the Async Lifecycle

Three key components govern the shutdown sequence according to the spdlog source code.

async_logger

The async_logger class holds a weak reference to the thread-pool and enqueues log and flush requests. In include/spdlog/async_logger.h, the destructor (or explicit drop operation) ensures remaining messages are flushed via backend_flush_() before the object is destroyed. This guarantees that any pending log entries are processed before the logger is removed from the registry.

thread_pool

Defined in include/spdlog/details/thread_pool.h and implemented in include/spdlog/details/thread_pool-inl.h, the thread pool owns the background worker threads that consume async_msg objects. When the pool is destroyed, it posts a special terminate message (async_msg_type::terminate) to each thread via post_async_msg_() and then join()s the threads. This graceful termination sequence ensures workers finish processing current messages before exiting.

registry

The global registry class in include/spdlog/details/registry.h and include/spdlog/details/registry-inl.h stores all loggers and owns the thread-pool instance. The registry::shutdown() method first stops the periodic flusher, then calls drop_all() to remove every logger, and finally resets the thread-pool (tp_.reset()), triggering the pool's destructor and thread joins.

The Shutdown Sequence Explained

When spdlog::shutdown() is invoked from include/spdlog/spdlog.h, the following occurs:

  1. Periodic Flusher Stop – The background thread that periodically calls flush() on all loggers is terminated first to prevent access to dying resources.
  2. Logger Removal – drop_all() removes every logger from the registry, triggering destructors that flush remaining messages via backend_flush_().
  3. Thread Pool Termination – The thread pool receives terminate messages for all worker threads, which finish their current work before joining.
  4. Resource Release – The thread pool is reset, releasing all associated memory and synchronization primitives.

Global Shutdown at Program Exit

Use this pattern when your application is terminating and you want to ensure all async loggers are properly drained:

#include "spdlog/spdlog.h"
#include "spdlog/async.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() {
    // Create async logger using global thread pool
    auto async_file = spdlog::create_async<spdlog::sinks::basic_file_sink_mt>(
        "async_file", "log.txt");
    
    async_file->info("application started");
    
    // Application work here...
    
    // Explicit shutdown before exit
    spdlog::shutdown();   // Drains queues, stops flusher, joins thread pool
    return 0;
}

Dropping a Single Logger

If you need to remove one logger while keeping others active, call spdlog::drop():

auto temp_logger = spdlog::create_async<spdlog::sinks::stdout_color_sink_mt>("temp");
temp_logger->info("Temporary log entry");

// Remove specific logger while leaving others running
spdlog::drop("temp");   // Drains its queue before erasing from registry

Custom Thread Pool Management

When using a custom thread pool, normal shutdown still applies:

auto tp = std::make_shared<spdlog::details::thread_pool>(8192, 2); // 2 workers
auto logger = std::make_shared<spdlog::async_logger>(
    "custom",
    spdlog::sinks::stdout_sink_mt::instance(),
    tp,
    spdlog::async_overflow_policy::block);

spdlog::register_logger(logger);
logger->info("Using custom thread pool");

// Shutdown cleans up custom pool too
spdlog::shutdown();

Edge Cases and Pitfalls

Calling shutdown() multiple times – The function is idempotent; subsequent calls become no-ops after the first successful shutdown.

Logging after shutdown – Any attempt to log after spdlog::shutdown() will recreate a new thread-pool if an async logger is requested, potentially leading to resource leaks if shutdown is not called again before the next termination.

Static-duration loggers – Loggers that live for the entire process duration (such as the default logger) must be explicitly dropped or shut down to avoid the static deinitialization order fiasco, where the thread pool might be destroyed before the logger.

Summary

  • Always call spdlog::shutdown() before program termination to ensure all async messages are written and background threads are joined.
  • The shutdown sequence stops the periodic flusher, drops all loggers (which flush their queues), and terminates the thread pool via terminate messages and join() calls.
  • Use spdlog::drop("name") to remove individual loggers while keeping the system running for other loggers.
  • Static loggers require explicit cleanup to prevent undefined behavior during static destruction.
  • Key implementation files: include/spdlog/async_logger.h, include/spdlog/details/thread_pool-inl.h, and include/spdlog/details/registry-inl.h contain the critical lifecycle logic.

Frequently Asked Questions

What happens if I don't call spdlog::shutdown()?

If you omit spdlog::shutdown(), background threads may continue running while static objects are being destroyed, potentially causing crashes or lost log messages. The thread pool destructor may run after dependent resources are gone, leading to undefined behavior. Additionally, any messages still in the queue when the process terminates will be lost.

Can I drop a single async logger without shutting down everything?

Yes. Call spdlog::drop("logger_name") to remove a specific logger from the registry. According to include/spdlog/details/registry-inl.h, this forces the logger to drain its queue before erasing the entry, allowing other async loggers to continue operating normally.

Is spdlog::shutdown() safe to call multiple times?

Yes. The registry::shutdown() implementation is idempotent. After the first call resets the thread pool and drops all loggers, subsequent calls simply return without attempting to clean up already-freed resources.

How do I handle static-duration async loggers?

For loggers with static storage duration (typically global or static instances), explicitly call spdlog::drop("logger_name") during your application's cleanup phase, or invoke spdlog::shutdown() before main() returns. This prevents the static deinitialization order fiasco where the logger might attempt to access the thread pool after it has been destroyed.

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 →