<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Odysseas</title>
    <description>The latest articles on DEV Community by Odysseas (@odygrd).</description>
    <link>https://dev.to/odygrd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1989497%2F36199921-4750-49a7-974f-d3a4dcf4c385.png</url>
      <title>DEV Community: Odysseas</title>
      <link>https://dev.to/odygrd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/odygrd"/>
    <language>en</language>
    <item>
      <title>Quill vs spdlog: Which C++ Logger Is Better for Low-Latency Applications?</title>
      <dc:creator>Odysseas</dc:creator>
      <pubDate>Fri, 19 Jun 2026 00:09:37 +0000</pubDate>
      <link>https://dev.to/odygrd/quill-vs-spdlog-which-c-logger-is-better-for-low-latency-applications-408</link>
      <guid>https://dev.to/odygrd/quill-vs-spdlog-which-c-logger-is-better-for-low-latency-applications-408</guid>
      <description>&lt;p&gt;Logging has a habit of ending up in the places you care about most.&lt;/p&gt;

&lt;p&gt;It starts as a few lines for visibility. Then those lines appear in request handling, market-data processing, matching loops, telemetry pipelines, and other code where predictable latency matters.&lt;/p&gt;

&lt;p&gt;At that point, a log statement is no longer just observability. &lt;strong&gt;It is work running on the same thread you are trying to keep fast.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A line like this can look harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order_id={} price={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important question is &lt;strong&gt;what happens before the caller continues&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Does it evaluate expensive arguments? Format text? Copy buffers? Allocate? Contend with other producer threads? Wait for queue space?&lt;/p&gt;

&lt;p&gt;For many applications, those costs are acceptable. For latency-sensitive systems, &lt;strong&gt;they are part of the latency budget&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/gabime/spdlog" rel="noopener noreferrer"&gt;spdlog&lt;/a&gt; is one of the best-known C++ logging libraries and a strong general-purpose choice. It is mature, easy to use, and has a broad feature set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/odygrd/quill" rel="noopener noreferrer"&gt;Quill&lt;/a&gt; was designed for a narrower problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How little work can a C++ logger leave on the caller thread while still producing rich, human-readable logs?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the lens for this comparison. The interesting difference is not which library has more features. &lt;strong&gt;It is where each library chooses to spend work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  At a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;spdlog async&lt;/th&gt;
&lt;th&gt;Quill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User-message formatting&lt;/td&gt;
&lt;td&gt;Producer thread&lt;/td&gt;
&lt;td&gt;Backend thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producer handoff&lt;/td&gt;
&lt;td&gt;Shared thread-pool queue&lt;/td&gt;
&lt;td&gt;Per-thread SPSC queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arguments for runtime-disabled levels&lt;/td&gt;
&lt;td&gt;Evaluated if the level was not compiled out&lt;/td&gt;
&lt;td&gt;Skipped by the macro-level runtime check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native synchronous mode&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend workers&lt;/td&gt;
&lt;td&gt;Configurable thread pool&lt;/td&gt;
&lt;td&gt;Single backend worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary focus&lt;/td&gt;
&lt;td&gt;General-purpose flexibility&lt;/td&gt;
&lt;td&gt;Low producer-side latency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These differences do not make one library universally better. They make each library better suited to different workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Logging Is Not One Design
&lt;/h2&gt;

&lt;p&gt;"Async logging" often means "file I/O happens on another thread." That is useful, but it is not enough to describe the cost paid by the caller.&lt;/p&gt;

&lt;p&gt;An async logger can still make the application thread:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;evaluate expensive arguments&lt;/li&gt;
&lt;li&gt;format the user message&lt;/li&gt;
&lt;li&gt;allocate or grow buffers&lt;/li&gt;
&lt;li&gt;contend with other producers&lt;/li&gt;
&lt;li&gt;block or drop when queues are full&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important point is that &lt;strong&gt;"async" describes where some work happens, not all of it&lt;/strong&gt;. Two async loggers can move very different amounts of work away from the caller.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Formatting: Caller Thread or Backend Thread?
&lt;/h2&gt;

&lt;p&gt;This is the first major difference, and it matters even when only one thread logs.&lt;/p&gt;

&lt;p&gt;In spdlog's templated logging path, &lt;code&gt;logger::log_&lt;/code&gt; first checks whether the runtime level or backtrace path needs the message. If not, it returns. If the message is needed, spdlog &lt;strong&gt;formats the user message on the caller thread&lt;/strong&gt; using &lt;code&gt;fmt::vformat_to&lt;/code&gt;, constructs a &lt;code&gt;log_msg&lt;/code&gt;, and then dispatches it.&lt;/p&gt;

&lt;p&gt;Stripped down to the relevant operations, that path looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;should_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;backtrace_enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vformat_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sink_it_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_msg&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In asynchronous mode, &lt;code&gt;async_logger::sink_it_&lt;/code&gt; posts that &lt;strong&gt;already-formatted message&lt;/strong&gt; to the thread pool. The thread pool wraps it in an &lt;code&gt;async_msg&lt;/code&gt;, and &lt;code&gt;log_msg_buffer&lt;/code&gt; owns the payload because the original formatted buffer was local to the caller.&lt;/p&gt;

&lt;p&gt;So in spdlog async mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runtime-disabled messages skip formatting inside spdlog&lt;/li&gt;
&lt;li&gt;enabled messages format the user message on the producer thread&lt;/li&gt;
&lt;li&gt;the formatted payload is copied into the async queue message&lt;/li&gt;
&lt;li&gt;the async backend handles sink dispatch, sink pattern formatting, flushing, and I/O&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps spdlog's backend model straightforward. Sinks receive a ready user message, and the async backend can focus on sink dispatch, flushing, and I/O.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quill moves the expensive part later.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A normal Quill &lt;code&gt;LOG_*&lt;/code&gt; call creates static call-site metadata, checks the logger level, serializes the arguments into the caller thread's queue, and returns. &lt;strong&gt;The backend worker later decodes those arguments and performs &lt;code&gt;{fmt}&lt;/code&gt; formatting.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The corresponding Quill path is closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;should_log_statement&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread_local_queue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// backend worker later decodes and formats&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in Quill's hot path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call-site metadata is static&lt;/li&gt;
&lt;li&gt;supported arguments are encoded into the frontend queue&lt;/li&gt;
&lt;li&gt;user message formatting is deferred to the backend worker&lt;/li&gt;
&lt;li&gt;sink pattern formatting and I/O also happen on the backend worker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why the single-thread benchmark rows later in this article matter. Quill's design is not only about avoiding contention between many producers. &lt;strong&gt;Deferring formatting changes the cost profile even for a single producer.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Macros: Source Location and Argument Evaluation
&lt;/h2&gt;

&lt;p&gt;Macros are sometimes treated as an API style preference. In logging, they can change &lt;strong&gt;runtime behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;spdlog's README starts with ordinary function calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;spdlog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Welcome to spdlog!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;spdlog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Some error message with arg: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That API is clean and familiar. The trade-off is that the convenience functions call the default logger with an empty &lt;code&gt;source_loc&lt;/code&gt;. If you want file, line, and function information in spdlog, you usually use macros such as &lt;code&gt;SPDLOG_INFO&lt;/code&gt; or pass &lt;code&gt;source_loc&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;spdlog macros provide two main things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source location capture through &lt;code&gt;__FILE__&lt;/code&gt;, &lt;code&gt;__LINE__&lt;/code&gt;, and function name&lt;/li&gt;
&lt;li&gt;compile-time filtering through &lt;code&gt;SPDLOG_ACTIVE_LEVEL&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source-location capture can also be disabled with &lt;code&gt;SPDLOG_NO_SOURCE_LOC&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With both spdlog and Quill, a level that is &lt;strong&gt;disabled at compile time&lt;/strong&gt; is removed completely. The corresponding macro expands to &lt;code&gt;(void)0&lt;/code&gt;, so its arguments are not evaluated.&lt;/p&gt;

&lt;p&gt;The important difference appears when the statement is &lt;strong&gt;compiled in but disabled at runtime&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With spdlog:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;SPDLOG_DEBUG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"payload={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_expensive_payload&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;build_expensive_payload()&lt;/code&gt; is &lt;strong&gt;evaluated&lt;/strong&gt; before control enters spdlog. spdlog can then check the logger's runtime level and skip formatting, but it cannot undo argument evaluation that has already happened.&lt;/p&gt;

&lt;p&gt;Quill's recommended API is macro-based:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order_id={} price={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;strong&gt;not just an API style choice&lt;/strong&gt;. Quill's macro checks the logger's runtime level before calling &lt;code&gt;log_statement(..., args...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With Quill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;LOG_DEBUG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"payload={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_expensive_payload&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when debug logging is disabled at runtime, &lt;code&gt;build_expensive_payload()&lt;/code&gt; &lt;strong&gt;is not called&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Quill also uses the macro to create static metadata containing the source location, function name, format string, tags, and level. Its default formatter includes &lt;code&gt;%(short_source_location)&lt;/code&gt;, so file and line are visible in the normal output path.&lt;/p&gt;

&lt;p&gt;If a level is disabled at compile time through &lt;code&gt;QUILL_COMPILE_ACTIVE_LOG_LEVEL&lt;/code&gt;, the corresponding Quill macro also expands to &lt;code&gt;(void)0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quill still provides macro-free logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order_id={} price={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mode is useful when ordinary function syntax is preferred or the call site is not performance critical. The trade-off is that macro-free calls &lt;strong&gt;always evaluate their arguments&lt;/strong&gt;, use runtime metadata handling, and cannot be fully compiled out like the macros.&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compile-time disabled:&lt;/strong&gt; neither library evaluates the arguments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime disabled with spdlog macros:&lt;/strong&gt; the arguments are still evaluated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime disabled with Quill macros:&lt;/strong&gt; the arguments are not evaluated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spdlog function calls&lt;/strong&gt; are ergonomic and familiar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;spdlog macros&lt;/strong&gt; add source location and compile-time removal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quill macros&lt;/strong&gt; add source metadata, compile-time removal, and runtime conditional argument evaluation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quill functions&lt;/strong&gt; remain available for less performance-critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Producer Queues: Shared MPMC vs Per-Thread SPSC
&lt;/h2&gt;

&lt;p&gt;This is the &lt;strong&gt;scaling&lt;/strong&gt; part of the comparison.&lt;/p&gt;

&lt;p&gt;In spdlog async mode, a logger is backed by a thread pool. The default async factory uses a global thread pool; if no global pool exists, it creates one with an 8192-slot queue and one worker thread.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;spdlog::init_thread_pool(...)&lt;/code&gt; customizes that global pool: queue size, worker count, and optional worker-thread callbacks. It &lt;strong&gt;does not give each producer thread its own queue&lt;/strong&gt;. Async loggers created through the global factory still post into the global thread pool queue.&lt;/p&gt;

&lt;p&gt;You can manually construct different async loggers with different thread pools, and then arrange for different application threads to use different loggers. That can reduce sharing if your application is structured that way, but it is an application-level convention. spdlog does not automatically give each producer thread its own queue.&lt;/p&gt;

&lt;p&gt;In spdlog 1.17.0, that queue is &lt;code&gt;details::mpmc_blocking_queue&amp;lt;async_msg&amp;gt;&lt;/code&gt;. It is a multi-producer, multi-consumer blocking queue implemented with a mutex and condition variables.&lt;/p&gt;

&lt;p&gt;That gives spdlog a compact async handoff model:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check runtime level.&lt;/li&gt;
&lt;li&gt;Format the user message.&lt;/li&gt;
&lt;li&gt;Copy the formatted payload into an async message.&lt;/li&gt;
&lt;li&gt;Push it into the thread-pool queue.&lt;/li&gt;
&lt;li&gt;Let backend worker threads write to sinks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is &lt;strong&gt;where producers meet&lt;/strong&gt;. When many threads log concurrently through the same thread pool, &lt;strong&gt;they meet at the shared queue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quill uses a different handoff.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each frontend thread owns a thread-local SPSC queue.&lt;/strong&gt; The application thread is the single producer for its queue, and the backend worker is the consumer. Thread A writes to thread A's queue. Thread B writes to thread B's queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Producer threads do not contend with each other on one shared logging queue.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backend worker drains all active queues, decodes events, orders them by timestamp within Quill's timestamp-ordering model, formats messages, forwards metrics, and writes to sinks.&lt;/p&gt;

&lt;p&gt;If your application has one producer thread, the queue contention advantage is less important. If your application has many hot threads, &lt;strong&gt;producer isolation becomes a much bigger part of the design&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ordering, Backpressure, and Backend Threads
&lt;/h2&gt;

&lt;p&gt;Backend design also affects &lt;strong&gt;ordering and backpressure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;spdlog's async thread pool can be configured with &lt;strong&gt;more than one worker&lt;/strong&gt;. That can be useful when backend throughput or sink work is the bottleneck. The handoff still goes through the thread pool's shared queue.&lt;/p&gt;

&lt;p&gt;Quill deliberately uses &lt;strong&gt;a single backend worker&lt;/strong&gt;. That worker drains the frontend queues, applies Quill's timestamp-ordering model, formats log messages, forwards metrics, and writes to sinks.&lt;/p&gt;

&lt;p&gt;That design is less about maximizing the number of backend worker threads and more about keeping producer threads isolated and making the backend pipeline predictable.&lt;/p&gt;

&lt;p&gt;Both libraries let you choose &lt;strong&gt;what happens when async logging cannot keep up&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;spdlog exposes async overflow policies such as &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;overrun_oldest&lt;/code&gt;, and &lt;code&gt;discard_new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Quill exposes the frontend queue model itself: &lt;code&gt;unbounded&lt;/code&gt; or &lt;code&gt;bounded&lt;/code&gt;, &lt;code&gt;blocking&lt;/code&gt; or &lt;code&gt;dropping&lt;/code&gt;. The default &lt;code&gt;unbounded blocking&lt;/code&gt; queue starts small, can grow, and eventually blocks if it reaches the configured maximum capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Native Sync Logging vs Async-First Logging
&lt;/h2&gt;

&lt;p&gt;This is one area where the libraries make &lt;strong&gt;different choices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;spdlog supports &lt;strong&gt;synchronous logging natively&lt;/strong&gt;. The default factory creates synchronous loggers. A synchronous spdlog logger formats and writes through its sinks on the calling thread. There is no backend logging thread, no async queue, and no need to coordinate with a worker before the log line reaches its destination.&lt;/p&gt;

&lt;p&gt;That is often exactly right for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;command-line tools&lt;/li&gt;
&lt;li&gt;build utilities&lt;/li&gt;
&lt;li&gt;desktop applications&lt;/li&gt;
&lt;li&gt;tests&lt;/li&gt;
&lt;li&gt;small services&lt;/li&gt;
&lt;li&gt;debugging workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quill is async-first.&lt;/strong&gt; Frontend threads capture log events, and the backend thread performs formatting and I/O. Quill &lt;strong&gt;does not provide a native synchronous mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For advanced integrations, Quill also has &lt;code&gt;ManualBackendWorker&lt;/code&gt;, which lets an application drive the backend from a user-managed thread or event loop. That can avoid Quill spawning its own backend thread, but it is still &lt;strong&gt;the same deferred backend pipeline rather than native synchronous logging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Separately, you can simulate synchronous behavior with &lt;code&gt;logger-&amp;gt;set_immediate_flush(1)&lt;/code&gt; or explicit &lt;code&gt;flush_log()&lt;/code&gt; calls. That makes the caller wait until the backend has processed and flushed the relevant log messages. It is useful for debugging, crash-sensitive areas, and controlled points in the program, but it is still &lt;strong&gt;an async architecture being synchronized&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is deliberate. Quill optimizes the normal logging path for hot threads. If you specifically want every log call to format and write on the caller thread, spdlog fits that model more directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Metrics Through the Same Backend Pipeline
&lt;/h2&gt;

&lt;p&gt;Quill also supports a feature most logging libraries do not attempt: &lt;strong&gt;metric publishing through the same async backend pipeline used for logs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You register &lt;code&gt;MetricMetadata&lt;/code&gt; once, bind a logger to a metric-capable sink, keep the returned metadata pointer, and publish &lt;code&gt;double&lt;/code&gt; samples from hot threads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MetricMetadata&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;requests_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frontend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;create_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;"requests_total_post_200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"requests_total"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="s"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;}});&lt;/span&gt;

&lt;span class="n"&gt;metrics_logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;publish_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the hot path, &lt;strong&gt;Quill queues a compact sample&lt;/strong&gt;. &lt;strong&gt;Metric names and labels are not serialized for every sample.&lt;/strong&gt; Inside Quill there is no second metrics worker; the backend worker forwards metric samples to metric-capable sinks through &lt;code&gt;Sink::write_metric()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The built-in &lt;code&gt;PrometheusSink&lt;/code&gt; integrates with &lt;code&gt;prometheus-cpp&lt;/code&gt; and supports counters, gauges, histograms, and summaries. Custom sinks can route the same samples to StatsD, OpenTelemetry, Graphite, or an in-process collector.&lt;/p&gt;

&lt;p&gt;That makes Quill more than &lt;strong&gt;"just log lines"&lt;/strong&gt; for systems where logs and metrics are both produced from hot application threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. User-Defined Types Are a Deliberate Trade-off
&lt;/h2&gt;

&lt;p&gt;Custom types are one place where spdlog can be simpler.&lt;/p&gt;

&lt;p&gt;Because spdlog formats the user message on the caller thread, a type that is formattable by &lt;code&gt;{fmt}&lt;/code&gt; can usually be logged directly. That is convenient and fits spdlog's general-purpose API.&lt;/p&gt;

&lt;p&gt;Quill defers formatting to the backend thread, so it must know &lt;strong&gt;how to safely carry enough data across threads before formatting happens&lt;/strong&gt;. Built-in and standard types are handled by Quill, but user-defined types sometimes need an explicit choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;DeferredFormatCodec&amp;lt;T&amp;gt;&lt;/code&gt; when copying the object to the backend is acceptable&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;DirectFormatCodec&amp;lt;T&amp;gt;&lt;/code&gt; when the object should be formatted immediately on the caller thread&lt;/li&gt;
&lt;li&gt;write a custom &lt;code&gt;quill::Codec&amp;lt;T&amp;gt;&lt;/code&gt; when only selected fields should be encoded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is more explicit than a plain &lt;code&gt;{fmt}&lt;/code&gt; formatter, but &lt;strong&gt;the explicitness is also the point&lt;/strong&gt;. Quill lets you decide whether a custom type belongs on the deferred-formatting path or whether formatting it on the hot path is the right compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Benchmark Results: Hot-Path Latency
&lt;/h2&gt;

&lt;p&gt;After the design differences, the main number for Quill's target audience is &lt;strong&gt;producer-thread latency&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much time did the logging call add to the application thread?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Quill's README publishes latency benchmarks measured in nanoseconds on a Linux system. Always benchmark your own workload, compiler, CPU, sink configuration, queue mode, and formatting patterns. The results are still useful because they reflect the architectural differences above.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benchmark&lt;/th&gt;
&lt;th&gt;Quill 50th&lt;/th&gt;
&lt;th&gt;Quill 99.9th&lt;/th&gt;
&lt;th&gt;spdlog 50th&lt;/th&gt;
&lt;th&gt;spdlog 99.9th&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numbers, 1 thread&lt;/td&gt;
&lt;td&gt;8 ns&lt;/td&gt;
&lt;td&gt;12 ns&lt;/td&gt;
&lt;td&gt;242 ns&lt;/td&gt;
&lt;td&gt;294 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbers, 4 threads&lt;/td&gt;
&lt;td&gt;11 ns&lt;/td&gt;
&lt;td&gt;21 ns&lt;/td&gt;
&lt;td&gt;528 ns&lt;/td&gt;
&lt;td&gt;973 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large strings, 1 thread&lt;/td&gt;
&lt;td&gt;11 ns&lt;/td&gt;
&lt;td&gt;20 ns&lt;/td&gt;
&lt;td&gt;216 ns&lt;/td&gt;
&lt;td&gt;259 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large strings, 4 threads&lt;/td&gt;
&lt;td&gt;8 ns&lt;/td&gt;
&lt;td&gt;27 ns&lt;/td&gt;
&lt;td&gt;515 ns&lt;/td&gt;
&lt;td&gt;939 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;std::vector&amp;lt;std::string&amp;gt;&lt;/code&gt;, 1 thread&lt;/td&gt;
&lt;td&gt;137 ns&lt;/td&gt;
&lt;td&gt;189 ns&lt;/td&gt;
&lt;td&gt;6749 ns&lt;/td&gt;
&lt;td&gt;7863 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;std::vector&amp;lt;std::string&amp;gt;&lt;/code&gt;, 4 threads&lt;/td&gt;
&lt;td&gt;101 ns&lt;/td&gt;
&lt;td&gt;158 ns&lt;/td&gt;
&lt;td&gt;6822 ns&lt;/td&gt;
&lt;td&gt;8903 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  9. Benchmark Results: Backend Throughput
&lt;/h2&gt;

&lt;p&gt;The Quill README also reports backend throughput :&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Elapsed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quill&lt;/td&gt;
&lt;td&gt;4.62 million msg/s&lt;/td&gt;
&lt;td&gt;866 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spdlog&lt;/td&gt;
&lt;td&gt;3.46 million msg/s&lt;/td&gt;
&lt;td&gt;1156 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Throughput answers a backend-side question: how many messages per second can the logging system format and write under this benchmark setup?&lt;/p&gt;

&lt;p&gt;That matters when the application produces logs faster than the backend can drain them for a sustained period. If that happens, frontend queues can grow, allocate, block, or drop messages depending on the configured queue mode.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;throughput is not the same as hot-path latency&lt;/strong&gt;. If the backend is keeping up, the producer thread mostly pays the cost of capturing the event into its queue. For low-latency systems, &lt;strong&gt;that producer-side cost is usually the first number to inspect&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Quill Looks Like
&lt;/h2&gt;

&lt;p&gt;The simple setup is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/LogMacros.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/SimpleSetup.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;simple_logger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello from {}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Quill"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For applications that want explicit backend and sink configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Backend.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Frontend.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/LogMacros.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Logger.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/sinks/ConsoleSink.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Backend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frontend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;create_or_get_logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frontend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;create_or_get_sink&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ConsoleSink&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"console_sink"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order_id={} price={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;123.45&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code still reads like normal structured logging. The important difference is what happens after the macro is called: &lt;strong&gt;the caller captures the event, and the backend does the heavier work&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where spdlog Fits Best
&lt;/h2&gt;

&lt;p&gt;Pick spdlog when you want a widely adopted general-purpose C++ logger with a familiar API, broad package-manager support, native synchronous logging, and many ready-made sinks.&lt;/p&gt;

&lt;p&gt;For command-line tools, build utilities, desktop applications, tests, and services with moderate logging volume, spdlog is often the pragmatic choice. Its &lt;code&gt;_st&lt;/code&gt; and &lt;code&gt;_mt&lt;/code&gt; sink variants are also convenient when you want explicit single-threaded or thread-safe sink types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Quill Fits Best
&lt;/h2&gt;

&lt;p&gt;Pick Quill when logging is part of the performance model, not an afterthought.&lt;/p&gt;

&lt;p&gt;If you are looking for a &lt;strong&gt;spdlog alternative for a latency-sensitive C++ system&lt;/strong&gt;, Quill is designed specifically around reducing the work performed on producer threads.&lt;/p&gt;

&lt;p&gt;That usually means you care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimizing producer-thread latency&lt;/li&gt;
&lt;li&gt;avoiding expensive argument evaluation when a level is disabled at runtime&lt;/li&gt;
&lt;li&gt;keeping &lt;code&gt;{fmt}&lt;/code&gt; formatting off hot threads&lt;/li&gt;
&lt;li&gt;logging from multiple hot threads without shared producer-queue contention&lt;/li&gt;
&lt;li&gt;publishing metrics through the same backend pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The application thread captures enough information to log later. The backend does the expensive work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;spdlog is a strong general-purpose C++ logger.&lt;/p&gt;

&lt;p&gt;Quill is a specialized low-latency C++ logger for applications where logging must be designed around the hot path.&lt;/p&gt;

&lt;p&gt;The difference is where formatting happens, when arguments are evaluated, how producers hand work to the backend, how queues apply backpressure, and what happens when many threads log at once.&lt;/p&gt;

&lt;p&gt;If this is the kind of C++ logging problem you are solving, try Quill, run the benchmarks on your own machine, and inspect the implementation.&lt;/p&gt;

&lt;p&gt;If the design looks useful for your systems, please consider starring the project so more C++ developers looking for low-latency logging can find it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Notes
&lt;/h2&gt;

&lt;p&gt;The comparison above was reviewed against Quill 12.0.0 and spdlog 1.17.0.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/odygrd/quill" rel="noopener noreferrer"&gt;https://github.com/odygrd/quill&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gabime/spdlog" rel="noopener noreferrer"&gt;https://github.com/gabime/spdlog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cpp</category>
      <category>logging</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Quill: High-Performance Asynchronous C++ Logging Library</title>
      <dc:creator>Odysseas</dc:creator>
      <pubDate>Wed, 28 Aug 2024 01:01:07 +0000</pubDate>
      <link>https://dev.to/odygrd/quill-high-performance-asynchronous-c-logging-library-lf1</link>
      <guid>https://dev.to/odygrd/quill-high-performance-asynchronous-c-logging-library-lf1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Efficient logging is crucial for application performance and debugging. Logging is an essential part of any software system, but it can often become a performance bottleneck, especially in low-latency applications. Quill addresses this challenge by offering an asynchronous, cross-platform logging solution that minimizes the impact on your application's hot path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Focus
&lt;/h2&gt;

&lt;p&gt;Quill is a feature-rich logging library built with performance in mind. Its design aims to provide faster logging capabilities compared to many traditional logging libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread-Local Lock-Free Ring Buffer:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Each thread is equipped with its own lock-free ring buffer, facilitating efficient, contention-free logging. This architecture eliminates inter-thread synchronization for log writes, drastically reducing overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile-Time Metadata Generation:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Quill generates essential log metadata—such as file name, line number, and format string—at compile time. By shifting this workload to compile time, runtime performance is significantly enhanced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binary Log Message Serialization:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Instead of formatting log messages on-the-fly, Quill serializes argument data in binary form directly into the ring buffer. This approach minimizes processing on the critical path of application threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Asynchronous Backend Processing:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A dedicated backend thread retrieves binary data from the ring buffers, formats the log messages, and outputs them to the designated sinks (e.g., files, console).&lt;/p&gt;

&lt;p&gt;This architecture enables Quill to deliver high performance by reducing work in application threads, capitalizing on compile-time optimizations, and leveraging asynchronous processing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example Usage
&lt;/h2&gt;

&lt;p&gt;Here's a basic example of how to use Quill in your C++ application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Backend.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Frontend.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/LogMacros.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/Logger.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"quill/sinks/ConsoleSink.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string_view&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Backend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frontend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;create_or_get_logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frontend&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;create_or_get_sink&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;quill&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ConsoleSink&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sink_id_1"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="n"&gt;LOG_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Hello from {}!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string_view&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"Quill"&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can try it on &lt;a href="https://godbolt.org/z/v1oa7Y3dY" rel="noopener noreferrer"&gt;Compiler Explorer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;To dive deeper into Quill or contribute to the project, visit the &lt;a href="https://github.com/odygrd/quill" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; or the &lt;a href="https://quillcpp.readthedocs.io" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; page.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>logging</category>
    </item>
  </channel>
</rss>
