When MAIN.threads_failed is nonzero, the Linux kernel is refusing Varnish’s pthread_create() calls. This is an OS-level resource limit preventing the worker thread pool from growing, not a Varnish configuration problem.
Do not confuse this with MAIN.threads_limited, which increments when Varnish declines to create a thread because thread_pool_max has been reached. threads_limited means Varnish chose not to create the thread. threads_failed means Varnish tried and the OS said no. The remediation differs: threads_limited requires raising a Varnish parameter; threads_failed requires fixing OS-level limits.
Any nonzero value indicates system misconfiguration. In a properly tuned deployment, this counter is always zero. When it increments, Varnish cannot scale its worker pool, requests queue, and sessions are eventually dropped.
What this means
Each worker thread handles one request at a time. When traffic increases and idle threads run out, Varnish creates new threads up to thread_pool_max per pool (default 5000). If pthread_create() fails during this ramp-up, threads_failed increments. The thread was not created, so the request that needed it either waits in the bounded queue or, if the queue is full, is dropped.
pthread_create() returns EAGAIN (errno 11, “Resource temporarily unavailable”) when the OS cannot allocate the thread. Three categories of OS limits cause this:
Process/thread count limit.
RLIMIT_NPROC(visible as “Max processes” in/proc/<pid>/limits) or a cgroup pids controller limit caps the total threads the Varnish user can create.Memory map limit. Each thread allocates memory maps for its stack. The kernel’s
vm.max_map_countcaps total maps per process, which indirectly caps total threads.Insufficient memory for thread stacks. Each thread stack costs 64KB-512KB depending on
thread_pool_stack. If the system cannot commit this memory (cgroup memory limits, strict overcommit, or actual exhaustion), thread creation fails.
flowchart TD
A["threads_failed nonzero"] --> B{"dmesg pids controller?"}
B -->|"Yes"| C["systemd TasksMax"]
B -->|"No"| D{"RLIMIT_NPROC at ceiling?"}
D -->|"Max processes hit"| E["ulimit -u too low"]
D -->|"Headroom available"| F{"vm.max_map_count low?"}
F -->|"Near limit"| G["kernel map count"]
F -->|"Adequate"| H["memory or cgroup limit"]The parameter thread_pool_fail_delay (default 0.200 seconds) controls how long Varnish pauses before retrying a failed thread creation. This prevents a tight retry loop but does not resolve the underlying OS limit.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| systemd cgroup pids controller | dmesg shows “fork rejected by pids controller”; threads_failed increments steadily | systemctl show varnish -p TasksCurrent -p TasksMax |
RLIMIT_NPROC / ulimit -u too low | “Max processes” in /proc/<pid>/limits is at or near the current thread count | cat /proc/$(pgrep -n varnishd)/limits | grep "Max processes" |
vm.max_map_count too low | Thread count plateaus at a round number well below thread_pool_max; no dmesg pids message | cat /proc/$(pgrep -n varnishd)/maps | wc -l vs sysctl vm.max_map_count |
| Memory exhaustion or overcommit | Committed_AS exceeds CommitLimit in /proc/meminfo; process RSS near cgroup or physical limit | grep -E 'Committed_AS|CommitLimit' /proc/meminfo |
thread_pool_add_delay too low (rapid-fire creation) | Bursts of threads_failed during traffic spikes; settles between spikes | varnishadm param.show thread_pool_add_delay |
Quick checks
All commands are read-only and safe for production.
# Confirm threads_failed is actually incrementing (take two readings 10s apart)
varnishstat -1 -f MAIN.threads_failed
# See the diagnostic message Varnish logs when thread creation fails
varnishlog -g raw -I Debug:thread
# Check if systemd's pids controller is the limiting factor
systemctl show varnish -p TasksCurrent -p TasksMax
# Check the RLIMIT_NPROC (Max processes) for the Varnish child process
cat /proc/$(pgrep -n varnishd)/limits | grep "Max processes"
# Check current thread count vs configured maximum
varnishstat -1 -f MAIN.threads -f MAIN.pools
varnishadm param.show thread_pool_max
# Check if kernel memory map count is the bottleneck
cat /proc/$(pgrep -n varnishd)/maps | wc -l
sysctl vm.max_map_count
# Check memory overcommit status
grep -E 'Committed_AS|CommitLimit' /proc/meminfo
sysctl vm.overcommit_memory
# Check the thread pool stack size (each thread costs this much memory)
varnishadm param.show thread_pool_stack
# Check whether the queue is filling as a consequence
varnishstat -1 -f MAIN.thread_queue_len -f MAIN.sess_dropped -f MAIN.req_dropped
How to diagnose
Confirm the counter is live, not stale. Take two
varnishstatreadings 10 seconds apart. IfMAIN.threads_failedis not changing, you may be looking at a historical value from before a previous fix.MAIN.*counters reset on child process restart.Check for the pids controller. Run
dmesg | grep -i pidsand look for “fork rejected by pids controller.” If present, systemd’sTasksMaxis the culprit. This is the most common cause on modern Linux running Varnish under systemd.Check RLIMIT_NPROC. Read
/proc/$(pgrep -n varnishd)/limitsand compare “Max processes” to the current thread count fromMAIN.threads. If they are close, the user-level process/thread limit is the constraint.Check vm.max_map_count. Count the memory maps for the Varnish child process with
cat /proc/$(pgrep -n varnishd)/maps | wc -l. Compare tosysctl vm.max_map_count. A rough rule of thumb is two maps per thread, so the default of 65530 limits you to approximately 32,000 threads.
Check memory availability. Compare
Committed_AStoCommitLimitin/proc/meminfo. Ifvm.overcommit_memory=2(strict no-overcommit), the kernel enforcesCommitLimitas a hard ceiling. Each thread stack (controlled bythread_pool_stack) consumes committed memory.Read the Varnish debug log. Run
varnishlog -g raw -I Debug:threadto see the exact failure. The typical message is “Create worker thread failed 11 Resource temporarily unavailable,” confirmingEAGAINfrompthread_create().
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
MAIN.threads_failed | Primary signal. Counts OS-level thread creation failures. | Any nonzero rate. |
MAIN.threads | Current total worker threads across all pools. | Plateauing below thread_pool_max x pools indicates an external ceiling. |
MAIN.threads_limited | Counts times Varnish hit its own thread_pool_max. If this increments alongside threads_failed, both problems coexist. | Nonzero rate means the pool is also undersized. |
MAIN.thread_queue_len | Requests waiting for a worker. When threads cannot be created, the queue grows. | Sustained nonzero value. |
MAIN.sess_dropped / MAIN.req_dropped | Downstream impact. When the queue fills because threads cannot be created, sessions or requests are dropped. | Any nonzero rate means users are being turned away. |
MGT.child_panic / MGT.child_died | If memory exhaustion is the root cause, the OOM killer may target the Varnish child process. | Any increment. |
Fixes
Identify which OS limit is binding before making changes.
systemd TasksMax (cgroup pids controller)
This is the most common cause on modern Linux. systemd v236+ applies a default TasksMax to services (typically 4915 or derived from system RAM), which caps total threads in the service cgroup.
Create a systemd override:
systemctl edit varnish
Add:
[Service]
TasksMax=infinity
Then apply:
systemctl daemon-reload
systemctl restart varnish
Warning: restarting Varnish flushes the cache. Schedule during a maintenance window if possible. Newer Varnish packages from the official repositories already ship with TasksMax=infinity in the unit file. Check whether this override is already present before adding it.
RLIMIT_NPROC (ulimit -u)
If “Max processes” in /proc/<pid>/limits is the binding constraint, raise it in the systemd unit:
[Service]
LimitNPROC=infinity
systemd services use LimitNPROC in the unit file, not PAM limits from /etc/security/limits.conf. Editing limits.conf has no effect on a systemd-managed Varnish process.
vm.max_map_count
If the process is hitting the kernel’s memory map limit:
# Apply immediately (affects future allocations, not existing threads)
sysctl -w vm.max_map_count=1048576
# Persist across reboots
echo 'vm.max_map_count=1048576' >> /etc/sysctl.d/99-varnish.conf
No restart of Varnish is needed. New thread creation will succeed once the map ceiling is raised.
Memory exhaustion or overcommit
If Committed_AS is near or above CommitLimit:
If
vm.overcommit_memory=2: Consider switching to0(heuristic overcommit, the kernel default) or1(always overcommit). Changing overcommit mode is a system-wide decision with tradeoffs beyond Varnish.If cgroup memory limit is binding: Raise
MemoryMaxfor the Varnish service, or ensure the machine has enough physical RAM for configured storage plus thread stacks plus overhead.If physical memory is genuinely exhausted: Reduce
thread_pool_stackto lower per-thread cost, reducethread_pool_max, or add memory. At 10,000 threads (5000 per pool, 2 pools), stack memory alone costs 640MB to 5GB.
thread_pool_add_delay tuning (mitigation, not fix)
If threads_failed occurs in bursts during rapid traffic spikes, Varnish may be creating threads faster than the OS can allocate them. Increasing thread_pool_add_delay smooths the creation rate:
# Live change, no restart needed
varnishadm param.set thread_pool_add_delay 0.002
This is a mitigation. If the OS limit is the binding constraint, smoothing the creation rate only delays the failure. Always address the OS limit first.
Prevention
Audit systemd limits on every new deployment. Check
TasksMaxandLimitNPROCbefore going live. Default values suit typical services, not high-concurrency caching proxies that may need thousands of threads.Calculate thread stack memory budget. Multiply
thread_pool_max x thread_pools x thread_pool_stackto estimate worst-case stack memory. Ensure the machine or cgroup has headroom for this plus cache storage plus overhead.Monitor
vm.max_map_countutilization. If your thread count is high, verify the map count ceiling is well abovethreads x 2.Alert on
threads_failed. This counter should always be zero. Any nonzero value warrants immediate investigation, not waiting for session drops.
How Netdata helps
Per-second granularity on
MAIN.threads_failedcatches short bursts that 15-second or 60-second polling intervals miss. Thread creation failures during traffic spikes can come and go within a single minute.Correlation with
MAIN.threads,MAIN.threads_limited, andMAIN.thread_queue_lenin a single view distinguishes OS-level refusal from config-level ceiling without switching tools.System-level metrics alongside Varnish counters correlate
threads_failedspikes with cgroup memory pressure, system-wide memory utilization, and process RSS in the same dashboard. This is what separates a Varnish problem from an OS constraint.Downstream impact visibility through
MAIN.sess_droppedandMAIN.req_droppedconfirms whether thread creation failures are actually affecting users or being absorbed by the queue.






