Overview

Quill is an asynchronous logging library built for C++ applications where every nanosecond on the hot path matters — trading systems, real-time engines, game servers, low-latency network stacks, and similar workloads.

Quill also lets you publish metric samples to an external metrics backend (Prometheus, StatsD, OpenTelemetry, or any in-process collector) through the same asynchronous pipeline used for logs, so hot threads never pay for metric formatting or export either. Quill is not a metrics collection or exposition system itself — it is the low-latency transport that hands samples off to whichever metrics backend your sink targets.

A dedicated backend worker handles log formatting, metric forwarding, and I/O, so even the slowest log line never blocks a hot thread on string conversion or disk writes. The frontend only captures a binary copy of the arguments (or a compact metric sample) into a thread-local lock-free queue; the backend does the rest.

At a glance:

  • Frontend: captures metadata and a binary copy of log arguments, or a compact metric sample, on the calling thread.

  • Backend: formats log messages and forwards metric samples on a dedicated worker thread.

  • Ordering: a single worker processes cached events by timestamp across all active queues and uses a configurable grace period to reduce cross-thread timestamp reordering without per-call locking.

  • Extensibility: sinks can handle logs, metrics, or both — the built-in library covers files, consoles, rotation, JSON, Syslog/systemd, Android, and Prometheus, and custom sinks plug in through a narrow virtual interface.

Quill design overview

Thread Safety

Logger is thread safe by default. The same instance can be used to log by any thread. Any thread can safely modify the active log level of the logger.

Logging types

For primitive types, std::string, and std::string_view, the library will perform a deep copy, and all formatting will occur asynchronously in the backend thread. For standard library types you need to include the relevant file under the quill/std folder. For user-defined types you can provide a Codec specialization to serialize the type or alternatively convert the type to a string on the hot path for non-latency-sensitive code.

See Recipes for examples of logging STL types and user-defined types.

Metrics

Quill can publish pre-registered metric samples through the same backend worker used for logs. Metric metadata is registered once and then reused by pointer on each publish call, so hot threads only enqueue a compact fixed-size sample record.

Metric-capable sinks receive samples through Sink::write_metric(). The bundled PrometheusSink is one option, but the metric API itself is backend-agnostic, so custom sinks can push metrics to any service or in-process collector you want.

Reliable Logging Mechanism

Quill utilizes a thread-local single-producer-single-consumer queue to relay log and metric events to the backend thread. By default, it uses an unbounded blocking queue with a small initial size to optimise performance. However, if the queue reaches its capacity, a new queue will be allocated, which may cause a slight performance penalty for the frontend.

The default unbounded queue can expand up to a size of FrontendOptions::unbounded_queue_max_capacity. If this limit is reached, the caller thread will block instead of intentionally dropping events. It’s possible to change the queue type within the FrontendOptions.

The queue size and type are configurable by providing a custom FrontendOptions class. See Frontend Options for details.

Manual Log Flushing

You can explicitly instruct the frontend thread to wait until all log entries up to the current timestamp are flushed using LoggerImpl::flush_log(). The calling thread will block until every log statement up to that point has been flushed.

Synchronized Logs for Debugging

Sometimes, synchronized logging is necessary during application debugging. This can be achieved by calling LoggerImpl::set_immediate_flush() to enable immediate flushing for a specific logger instance.

This causes the calling thread to pause until the log is processed and written to the log file by the backend thread before proceeding, which may have a notable impact on performance.

Note that the immediate flush feature can be completely disabled at compile time by defining QUILL_ENABLE_IMMEDIATE_FLUSH=0, which eliminates the conditional branch from the hot path for better performance when immediate flushing is never needed.

Handling Application Crashes

During normal program termination, the library ensures all messages are logged as it goes through the BackendWorker destructor.

However, in the event of an application crash, some log messages may still be lost.

The library provides an optional built-in signal handler that can help preserve logs in many common crash and termination scenarios. It can be enabled by passing SignalHandlerOptions to Backend::start().

quill::Backend::start(quill::BackendOptions{}, quill::SignalHandlerOptions{});

On POSIX systems, any thread that may run the built-in handler should either:

  • have already logged at least once,

  • have called FrontendImpl::preallocate(), or

  • have the handled signals blocked so the handler does not run on that thread.

If you use your own POSIX signal handler, keep it minimal and avoid calling the general logging or flush APIs from an arbitrary signal context unless you have validated that approach for your platform and process state.

SignalHandlerOptions allows you to configure:

  • catchable_signals — the list of signals to handle (defaults to SIGTERM, SIGINT, SIGABRT, SIGFPE, SIGILL, SIGSEGV).

  • timeout_seconds — alarm timeout to prevent the process from hanging in the signal handler (Linux only, defaults to 20 seconds).

  • logger_name — the logger to use for crash reporting. If empty, the signal handler automatically selects the first valid logger.

  • excluded_logger_substrings — logger names containing these substrings are skipped during automatic selection (defaults to {"__csv__"}).

On Windows, Backend::start() installs structured exception handling and a process-wide console control handler. CRT signal handlers are thread-specific; call quill::init_signal_handler<FrontendOptions>() on each frontend/user thread that needs CRT signal handling. Do not install the CRT signal handler on the backend worker thread.

Log Messages Timestamp Order

The library employs a single worker backend thread that processes cached log messages from all queues by timestamp before printing them to the log file. A configurable timestamp-ordering grace period gives frontend threads a short window to publish already-timestamped events, reducing cross-thread timestamp reordering.

Number of Backend Threads

Quill prioritizes low latency over high throughput, hence it utilizes only one backend thread to process all logs efficiently. Multiple backend threads are not supported.

Starting and stopping the backend from different threads at the same time is also unsupported. Call Backend::start() and Backend::stop() in a coordinated way rather than concurrently.

Latency of the First Log Message

Upon the first log message from each thread, the library allocates a queue dynamically. For minimizing latency with the initial log, consider calling FrontendImpl::preallocate().

Configuration

Quill offers various customization options, well-documented for ease of use.

Frontend (caller-thread)

The frontend is the calling thread on the user side which issues log statements. It includes:

  • Loggers: A Logger contains a format pattern and can include one or multiple output Sinks.

  • Sinks: The Sink serves as the output destination, such as a file, console, or other sources.

Log messages are written using macros that accept a logger as their first argument, followed by a format string. The backend utilizes the {fmt} library for formatting.

When invoking a LOG_ macro:

  1. Creates a static constexpr metadata object to store Metadata such as the format string and source location.

  2. Pushes the data to the SPSC lock-free queue. For each log message, the following variables are pushed:

Variable

Description

timestamp

Current timestamp

Metadata*

Pointer to metadata information

Logger*

Pointer to the logger instance

DecodeFunc

A pointer to a templated function containing all the log message argument types, used for decoding the message

Args…

A serialized binary copy of each log message argument that was passed to the LOG_ macro

When invoking METRIC(...) or LoggerImpl::publish_metric():

  1. Reuses previously registered MetricMetadata instead of serializing metric names or labels on the hot path.

  2. Pushes a compact fixed-size sample record to the same SPSC queue.

Variable

Description

timestamp

Current timestamp

MetricMetadata*

Pointer to the registered metric metadata

Logger*

Pointer to the logger instance

value

The double metric sample

Backend

The backend consists of a single backend thread which takes care of formatting log statements, forwarding metric samples, and performing the IO writing to files. Consumes each event from the SPSC queue, retrieves all the necessary information, and then formats log messages or forwards metric samples to Sink::write_metric. Subsequently, forwards the log or metric event to all Sinks associated with the Logger.

See Also

  • Quick Start for the shortest path to working logs.

  • Guides for sinks, metrics, formatters, JSON, filters, and more.

  • Recipes for common tasks and code examples.