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
# INFO/DEBUG messages missing during high load
# Only WARN and ERROR appear in logs
# No warning that messages were droppedOr explicit warning:
18:45:23,234 |-WARN in ch.qos.logback.classic.AsyncAppender[ASYNC] - Queue full, discarding events
# INFO and DEBUG events dropped, only WARN+ loggedCommon 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