I am currently running into an issue on Kong Gateway where a custom Lua plugin is failing to reliably flush buffered request telemetry payloads to an external collector under high concurrent load, and I am trying to figure out if this is related to how the nginx worker thread lifecycle handles asynchronous sockets.
For some context on our architecture, our team uses Kong Gateway to route and monitor incoming API traffic from an external game state synchronization server. Our backend services process real-time events and user automation data from sandboxed testing environments. We manage a fleet of QA instances where developers run performance scripts through a roblox executor setup to test server-side anti-cheat logic and bandwidth limits under heavy traffic. We keep our core developer guidelines and public documentation organized on an open free index workspace, and we pass these event metrics through our Kong API Gateway layers to authenticate and rate-limit client requests.
The primary issue we are facing is that during peak traffic bursts, our custom logging phase plugin begins dropping background log batches. We are using timer wrappers to spawn background tasks and dispatch JSON payload buffers via TCP sockets, but when request rates spike, Kong starts throwing errors indicating that there are too many pending timers in the error log. The worker processes seem to hit their timer limit, causing pending telemetry batches to get discarded before they ever reach the network layer.
In addition to the dropped telemetry timers, we are noticing a secondary memory buildup on the Gateway nodes. When these timer queues back up under load, RAM usage across the worker processes climbs steadily and fails to release back to baseline levels even after client traffic settles down, eventually forcing us to reload the Kong process to reclaim memory.
Has anyone else encountered timer queue exhaustion when using custom Lua plugins to stream high-volume request metrics through Kong Gateway, and what is the recommended buffer configuration or batching pattern to handle high-concurrency log flushing without overloading the worker event loop?
@lara887 I passed your question through our documentation agent, please see the repsonse for information regarding your issue.
Timer Exhaustion & Batch Queue Configuration in Kong Gateway
This is a well-known issue with custom Lua plugins that spawn individual timers per request for background log dispatch. Kong’s built-in plugins have addressed exactly this problem, and the recommended pattern is to use internal batch queues instead of per-request timers.
Root Cause
When a custom plugin spawns a new timer for every request during the log phase, timer counts grow proportionally with traffic. Under high concurrency, this exhausts the worker’s available timer slots, producing the too many pending timers error and causing batches to be silently dropped. [Gateway 2.8.0.0]
This is precisely why Kong added batch queues to the Datadog and StatsD plugins — to fix the same lua_max_running_timers are not enough error caused by creating a new timer per request during the log phase. [Gateway 2.8.2.2]
Recommended Pattern: Use a Single Queue with Batching
Instead of spawning a timer per request, the correct pattern is to enqueue entries and use a single background timer to drain the queue in batches. This is how all of Kong’s built-in logging/telemetry plugins (HTTP Log, OpenTelemetry, Datadog, StatsD, Zipkin) work. [Plugin Queues]
Key queue parameters you should configure for your custom plugin (or migrate to a built-in plugin if applicable):
| Parameter | Purpose | |—|—| | queue.max_batch_size | Max entries sent per upstream request (reduces upstream concurrency) | | queue.max_entries | Max entries waiting in queue (default: 10,000) | | queue.max_bytes | Max bytes waiting in queue | | queue.concurrency_limit | Number of background delivery timers (default: 1) | | queue.initial_retry_delay | Wait before first retry on failure | | queue.max_retry_delay | Cap on exponential backoff | | queue.max_retry_time | Total time before giving up on a failing batch |
Queues are per-worker and in-memory only. They are not shared between Nginx worker processes. For capacity planning, multiply your queue limits by the number of workers. [HTTP Log Queuing]
Memory buildup is expected when the upstream collector is unavailable and the queue fills up. The queue will warn at 80% capacity and begin dropping the oldest entries once max_entries is reached. This is the designed behavior to bound memory usage. [Plugin Queues]
On shutdown, queues are flushed automatically, so in-flight batches are not lost during graceful restarts. [OpenTelemetry Queuing]
Queued content is not durable — power loss or unexpected worker crashes will lose buffered data. [HTTP Log Queuing]
Practical Recommendations
Refactor your custom plugin to use a single shared queue with one background timer, rather than spawning a timer per log entry.
Tune queue.max_batch_size — Kong increased the default for OpenTelemetry to 200 in 3.6.1.0 as a performance improvement. [Gateway 3.6.1.0]
Size queue.max_entries based on your expected burst duration × request rate per worker. The default of 10,000 is a reasonable starting point. [Plugin Queues]
Test under load with the upstream collector intentionally unavailable to observe peak memory consumption before setting limits for production. [Plugin Queues]
If migrating to a built-in plugin is feasible (e.g., HTTP Log for JSON payloads over HTTP/HTTPS), that would be the most straightforward path since the queuing infrastructure is already implemented and battle-tested.