Introduction

Logback's AsyncAppender buffers log events in a bounded queue and dispatches them asynchronously to appenders, improving application throughput by decoupling logging from I/O. However, when the queue fills up (default size 256), AsyncAppender discards TRACE, DEBUG, and INFO level messages to protect application performance, logging only WARN and ERROR. This means important debugging context is silently lost during high-load periods -- exactly when you need it most. The fix requires configuring queue size, discard thresholds, and optionally blocking behavior to prevent message loss.

Symptoms

bash
# INFO/DEBUG messages missing during high load
# Only WARN and ERROR appear in logs
# No warning that messages were dropped

Or explicit warning:

bash
18:45:23,234 |-WARN in ch.qos.logback.classic.AsyncAppender[ASYNC] - Queue full, discarding events
# INFO and DEBUG events dropped, only WARN+ logged

Common Causes

  • Default queue size 256 too small: High log volume fills queue quickly
  • neverBlock mode discards messages: Default behavior drops lower-level logs
  • Slow appender cannot keep up: File or network appender slower than log production rate
  • No includeCallerData: Caller data not captured, making debugging harder
  • Single appender for all levels: No separation between info and error logs
  • Worker thread blocked: Async appender worker blocked on downstream appender

Step-by-Step Fix

Step 1: Configure async appender with proper queue size

```xml <configuration> <!-- Console appender --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>

<!-- File appender --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/app/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>/var/log/app/app-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>

<!-- Async appender wrapping file appender --> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>1024</queueSize> <!-- Increased from default 256 --> <discardingThreshold>0</discardingThreshold> <!-- 0 = never discard --> <includeCallerData>true</includeCallerData> <!-- Include stack traces --> <neverBlock>false</neverBlock> <!-- Block instead of discarding --> <appender-ref ref="FILE"/> </appender>

<root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="ASYNC"/> </root> </configuration> ```

Step 2: Separate error logging from async

```xml <!-- Errors should always be synchronous for reliability --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/app/error.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <!-- ... rolling policy ... --> </appender>

<!-- Info/debug goes through async --> <appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>2048</queueSize> <discardingThreshold>20</discardingThreshold> <!-- Keep last 20 when queue nearly full --> <appender-ref ref="INFO_FILE"/> </appender>

<root level="INFO"> <appender-ref ref="ERROR_FILE"/> <!-- Synchronous for errors --> <appender-ref ref="ASYNC_INFO"/> <!-- Async for info/debug --> </root> ```

Prevention

  • Set discardingThreshold to 0 to prevent message loss (or set to a known value)
  • Size queue based on expected peak log volume per second
  • Use synchronous appenders for ERROR level to guarantee critical log delivery
  • Monitor async appender queue size with JMX or metrics
  • Enable includeCallerData only when needed (adds overhead)
  • Set neverBlock=false if log delivery is more important than application performance
  • Test log volume under load to determine appropriate queue size