Increase Netty workerThreads or accept acceptor‑queue‑induced latency bursts under high concurrency?
0 reputation · 07 Apr 2023, 04:45 UTC
0 reputation · 07 Apr 2023, 04:45 UTC
The goal is to keep request latency low when a Ktor server handles many concurrent connections while preserving overall throughput.
With the Netty engine, the default acceptor queue size caps the number of requests that can be processed simultaneously, and there is no stable API to adjust this queue independently of the worker thread count. Raising the workerThreads setting allows more concurrent connections but introduces additional context‑switch overhead, which can increase latency if the pool is oversized.
Given this trade‑off, it is unclear what workerThread configuration yields the best latency‑throughput balance, whether monitoring the acceptor queue depth would help predict bursts, or if alternative engine tuning might be preferable.
What workerThread count minimizes latency spikes without causing excessive context switching? Is there a reliable way to observe acceptor queue utilization under load?
32775 reputation · 07 Apr 2023, 09:12 UTC
There is no single workerThread count that works for every Ktor application. The optimal size depends on the number of CPU cores, the blocking‑ness of your request handlers, and the load pattern. A practical rule of thumb is to start with two worker threads per CPU core for I/O‑bound workloads and then adjust based on measured p99 latency and context‑switching activity. Raising the pool beyond the point where latency stops improving will only increase context switches and memory pressure.
The acceptor backlog (the OS TCP listen queue) is not exposed as a stable Ktor or Netty API. When it overflows, new connections are dropped or delayed, which shows up as latency spikes. Because you can’t change it through Ktor, you need to monitor it with OS‑level tools and let the results guide whether you need a larger backlog or a smaller worker pool.
nproc or lscpu | grep CPU(s).
NettyApplicationEngineConfig to twice that number.
engine { workerThreads = 2 * Runtime.getRuntime().availableProcessors() }
vmstat -s or pidstat -w -p <pid>)Because the backlog is managed by the OS, you can’t read it from Netty. Use these commands to get an indirect view:
ss -ltn state listening -i – shows the backlog size and the number of queued connections for each listening socket.
netstat -tanp | grep LISTEN – older but still useful on many systems.
cat /proc/net/tcp contains backlog information in the sixth column of each line.
Correlate these metrics with spikes in request latency from your load test. If the backlog is often full, consider raising the OS backlog value (e.g., sysctl -w net.core.somaxconn=65535) or reducing the rate of new connections by throttling clients or adding a connection pool on the client side.
pipeline.intercept(HttpSendPipeline.After) { ... } to avoid overwhelming the event loop.
Could you tell me how many logical CPU cores are on the machine running the Ktor server and whether your request handlers perform blocking operations? This information will help fine‑tune the workerThreads count and decide whether a larger backlog or a smaller pool is more appropriate.
Use comments to ask for clarification. Post a solution as an answer.
32,775 reputation · 07 Apr 2023, 14:53 UTC
Netty exposes the OS backlog through ChannelOption.SO_BACKLOG on the ServerBootstrap. The kernel caps values above SOMAXCONN, so the actual queue may be smaller than requested.
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
Use ss to see how many half‑open connections the kernel is holding:
ss -tanp | grep LISTEN
# Look for the SYN_RECV count next to the listening port.
If the SYN_RECV count is close to the backlog value, the queue is saturated and you should increase SO_BACKLOG or the number of worker threads.
When JMX is enabled, EventLoopGroup thread‑queue sizes can be read and cross‑checked with the OS view to confirm the pool is not the bottleneck.