Skip to content

Queue-Based Async Logging ​

Logly supports queue-based async logging to decouple log calls from actual I/O operations. This prevents logging from blocking your application, especially for high-throughput or network-based sinks.

enqueue=True in add() ​

Enable queue-based logging by setting enqueue=True when adding a sink:

python
from logly import logger

logger.remove()

# Enable background worker for this sink
logger.add("app.log", enqueue=True)

# All log calls are now non-blocking
for i in range(10000):
    logger.info("Message {}", i)

# Must complete before exit to flush the queue
logger.complete()

WARNING

Always call logger.complete() before your process exits when using enqueue=True. Without it, queued messages may be lost.

How It Works ​

When enqueue=True:

  1. Each log call places the message in a thread-safe queue
  2. A background worker thread processes messages from the queue
  3. The calling thread continues immediately without waiting for I/O
  4. logger.complete() drains the queue and waits for all messages to be processed

Background Worker Threads ​

Logly spawns a single background worker thread per sink with enqueue=True:

python
from logly import logger
import threading

logger.remove()

# This sink has a background worker
logger.add("app.log", enqueue=True)

# This sink is synchronous
logger.add("stderr")


def worker():
    for i in range(100):
        logger.info("Thread {} message {}", threading.current_thread().name, i)


# Multiple threads can log simultaneously
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# Flush all queued messages
logger.complete()

Worker Behavior ​

  • Single worker per sink: Each sink with enqueue=True gets its own background thread
  • Thread-safe queue: Multiple threads can safely enqueue messages
  • Order preservation: Messages are processed in FIFO order
  • Error handling: Worker errors are caught and logged to stderr

Queue Behavior ​

When the queue fills up during high-throughput logging, Logly applies backpressure by blocking the calling thread until space is available. This ensures no messages are lost.

python
from logly import logger

logger.remove()

logger.add(
    "app.log",
    enqueue=True,
    rotation="100 MB",
)

# If queue is full, this call blocks until space is available
for i in range(100000):
    logger.info("Message {}", i)

logger.complete()

complete() for Flushing ​

The complete() method waits for all pending log messages to be processed:

python
from logly import logger

logger.remove()

logger.add("app.log", enqueue=True)
logger.add("errors.log", enqueue=True)

logger.info("Important message")
logger.error("Error message")

# Wait for all queued messages to be written
logger.complete()

# All messages are now guaranteed to be written
print("Done")

In Application Lifecycle ​

python
from logly import logger


def main():
    logger.add("app.log", enqueue=True)

    try:
        run_application()
    finally:
        # Always flush before exit
        logger.complete()


if __name__ == "__main__":
    main()

With Multiple Sinks ​

python
from logly import logger

logger.remove()

# Multiple queued sinks
logger.add("app.log", enqueue=True)
logger.add("errors.log", enqueue=True, level="ERROR")
logger.add("audit.log", enqueue=True, level="SUCCESS")

logger.info("Info message")
logger.error("Error message")
logger.success("Success message")

# Flush all sinks at once
logger.complete()

Timeout ​

python
from logly import logger

logger.remove()
logger.add("app.log", enqueue=True)

logger.info("Message")

# Wait for all pending messages to be processed
logger.complete()

TIP

Call complete() in your application's shutdown handler or finally block to ensure all messages are flushed.

Thread Safety with contextualize() ​

Use contextualize() to attach thread-specific context when logging from multiple threads:

python
import threading
from logly import logger

logger.remove()

logger.add("app.log", enqueue=True)

def worker(worker_id: int):
    with logger.contextualize(worker_id=worker_id):
        logger.info("Worker started")
        # Each thread has its own context
        for i in range(10):
            logger.info("Processing item {}", i)
        logger.info("Worker finished")

threads = [threading.Thread(target=worker, args=(i, for i in range(4))]
for t in threads:
    t.start()
for t in threads:
    t.join()

logger.complete()

Async Context ​

python
import asyncio
from logly import logger

logger.remove()
logger.add("app.log", enqueue=True)


async def handle_request(request_id: str):
    with logger.contextualize(request_id=request_id):
        logger.info("Starting request")
        await asyncio.sleep(0.1)
        logger.info("Request complete")


async def main():
    await asyncio.gather(
        handle_request("req-1"),
        handle_request("req-2"),
        handle_request("req-3"),
    )


asyncio.run(main())
logger.complete()

Bind vs Contextualize ​

Featurebind()contextualize()
ScopePersistentScoped to block
Thread safetyCreate new loggerThread-local
Use caseLogger-level contextRequest-level context

Performance Characteristics ​

Throughput Comparison ​

python
from logly import logger
import time

logger.remove()

# Synchronous logging
logger.add("sync.log")
start = time.time()
for i in range(100000):
    logger.info("Sync message {}", i)
sync_time = time.time() - start
logger.complete()

# Queue-based logging
logger.add("async.log", enqueue=True)
start = time.time()
for i in range(100000):
    logger.info("Async message {}", i)
logger.complete()
async_time = time.time() - start

print(f"Sync: {sync_time:.2f}s")
print(f"Async: {async_time:.2f}s")

Memory Usage ​

python
from logly import logger
import sys

logger.remove()

# Queue uses a bounded buffer by default
logger.add("app.log", enqueue=True)
logger.info("Message")
logger.complete()

Latency ​

ModeTypical LatencyP99 Latency
Sync< 1ms< 5ms
Enqueue< 1ms< 10ms

When to Use Enqueue vs Sync ​

Use Enqueue When ​

ScenarioReason
High-throughput loggingAvoid blocking application threads
Network sinksI/O latency doesn't block app
File rotation with many writersSingle writer prevents contention
Latency-sensitive codeLogging overhead must be minimal
Background tasksDon't slow down main thread

Use Sync When ​

ScenarioReason
Simple scriptsNo background thread overhead
Low-throughput loggingSimple is better
Debug loggingImmediate output guaranteed
Small filesNo queue overhead needed
Error logging onlyCritical messages must be immediate

Decision Guide ​

python
from logly import logger

logger.remove()

# High-throughput: use enqueue
logger.add("app.log", enqueue=True, rotation="100 MB")

# Network sink: use enqueue
logger.add(http_sink, enqueue=True)

# Low-throughput: sync is fine
logger.add("errors.log", level="ERROR")

# Debug: sync for immediate output
logger.add("stderr", level="DEBUG")

Common Patterns ​

High-Throughput Logging ​

python
from logly import logger
import time

logger.remove()

# High-throughput with enqueue
logger.add(
    "app.log",
    enqueue=True,
    rotation="100 MB",
    retention="7 days",
    compression="gzip",
)


def process_batch(items):
    """Process a batch of items with logging."""
    for item in items:
        logger.debug("Processing {}", item)
        result = transform(item)
        logger.trace("Transformed {}", result)
    logger.info("Batch complete: {} items", len(items))


# Process millions of items
for batch in get_batches():
    process_batch(batch)

logger.complete()

Web Server Logging ​

python
from logly import logger

logger.remove()

# Request logging (high throughput)
logger.add(
    "access.log",
    enqueue=True,
    format="{time:YYYY-MM-DD HH:mm:ss} | {level:<8} | {message}",
)

# Error logging (must not lose)
logger.add(
    "errors.log",
    enqueue=True,
    level="ERROR",
    format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level:<8} | {message}",
    backtrace=True,
)


def handle_request(request):
    with logger.contextualize(request_id=request["id"]):
        logger.info("{} {} {}", request["method"], request["path"], request["status"])

Background Task Worker ​

python
from logly import logger
import threading

logger.remove()

logger.add("worker.log", enqueue=True)


def background_worker():
    """Background worker with its own logging context."""
    with logger.contextualize(worker_id=threading.current_thread().name):
        while True:
            task = get_task()
            if task is None:
                break
            logger.info("Processing task {}", task["id"])
            try:
                result = execute(task)
                logger.success("Task {} completed", task["id"])
            except Exception:
                logger.exception("Task {} failed", task["id"])


# Start multiple workers
workers = [threading.Thread(target=background_worker) for _ in range(4)]
for w in workers:
    w.start()
for w in workers:
    w.join()

logger.complete()

Application Lifecycle ​

python
from logly import logger
import signal
import sys


def shutdown(signum, frame):
    """Graceful shutdown handler."""
    logger.warning("Received signal {}, shutting down", signum)
    logger.complete()
    sys.exit(0)


signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)


def main():
    logger.remove()

    logger.add("app.log", enqueue=True)
    logger.add("errors.log", enqueue=True, level="ERROR")

    logger.info("Application started")

    try:
        run_application()
    except KeyboardInterrupt:
        logger.warning("Interrupted by user")
    except Exception:
        logger.exception("Fatal error")
    finally:
        logger.complete()


if __name__ == "__main__":
    main()

Mixed Sync and Async ​

python
from logly import logger

logger.remove()

# Sync for critical errors (immediate output)
logger.add(
    "stderr",
    level="ERROR",
    format="<red>{time:HH:mm:ss}</red> | <level>{level:<8}</level> | {message}",
    colorize=True,
)

# Async for high-throughput files
logger.add(
    "app.log",
    enqueue=True,
    rotation="daily",
)

# Async for network sink
logger.add(http_sink, enqueue=True)

logger.info("This goes to app.log and http_sink (async)")
logger.error("This goes to stderr (sync), app.log (async), and http_sink (async)")
logger.complete()

Enqueue with Rotation ​

python
from logly import logger

logger.remove()

logger.add(
    "app.log",
    enqueue=True,
    rotation="daily",
    retention=30,
    compression="gzip",
)

for i in range(100000):
    logger.info("High-volume message {}", i)

logger.complete()

Released under the MIT License.