The symptom looks like a capacity problem: Apache processes pinned near 100% CPU, request latency climbing, and no obvious cause in the access log. Traffic is up, but not absurdly. The workers are not stuck on a backend. The scoreboard is busy but not exhausted. And yet the box is melting.

On HTTPS-heavy servers, the usual suspect is the TLS handshake. A full TLS handshake with an RSA-2048 certificate costs on the order of 15 ms of CPU time per connection. Session resumption cuts that cost by roughly 10x. When resumption works, repeat clients skip the expensive public-key operation entirely. When resumption is broken, every connection pays the full price, and at a few hundred new connections per second the math stops working.

That is the HTTPS CPU wall: the point where the cost of full handshakes per second exceeds the CPU you have, and no amount of worker tuning gets it back. This article covers how to confirm that broken session resumption is the cause, why resumption breaks in Apache, and how to fix it.

What this means

TLS offers two ways to resume a session: session IDs (server-side state) and session tickets (RFC 5077, client-held encrypted state). Apache 2.4’s defaults quietly undermine the first mechanism:

  • SSLSessionCache defaults to none. The inter-process session cache is disabled entirely out of the box.
  • Without a shared cache, a session ID is only valid in the child process that created it. With many children and a load-balanced client reconnecting to a random child, cache hits are rare.
  • Session tickets are on by default (SSLSessionTickets on) and work across children, but they have their own traps: Apache generates one random ticket key at startup and never rotates it, and with TLS 1.3 a graceful restart can silently break ticket-based resumption.

The result is a server where most reconnecting clients do a full handshake anyway. Because a full handshake is the single most CPU-intensive thing Apache does, the failure shows up as a CPU problem, not as a TLS error. Nothing is logged. A silently full or absent session cache means old sessions evicted, resumption rate drops, CPU increases, and no error logged anywhere.

flowchart TD
  A[New connection to :443] --> B{Session resumed?}
  B -- yes --> C[Abbreviated handshake
low CPU] B -- no --> D[Full handshake
~15 ms CPU with RSA-2048] E[Resumption broken:
SSLSessionCache none,
shmcb missing or too small,
TLS 1.3 + graceful restart] --> B D --> F[CPU saturated at
moderate connection rate] F --> G[Latency climbs,
handshake queue grows]

Common causes

CauseWhat it looks likeFirst thing to check
SSLSessionCache none (the default)Low resumption rate since forever; CPU tracks new-connection rate linearlyapachectl -t -D DUMP_RUN_CFG or grep the config for SSLSessionCache
shmcb cache too smallResumption worked at low traffic, degraded as traffic grew; no error loggedCache size vs concurrent session count; test resumption under load
mod_socache_shmcb not loaded (2.2 to 2.4 upgrade)Config references shmcb but startup fails or cache is silently unsupportedError log for SSLSessionCache: 'shmcb' session cache not supported
TLS 1.3 resumption broken after graceful restartResumption works after a full restart, stops after apachectl gracefulTest openssl s_client -reconnect before and after a graceful restart
Session tickets disabled, cache not sharedFull handshakes for every client that lands on a different childCheck SSLSessionTickets and whether any shared cache is configured
RSA-2048 certificate on a high-connection-rate vhostHigh per-handshake cost even when resumption works; CPU per connection highopenssl s_client shows cert type; openssl speed rsa2048 ecdsap256 shows the gap
No HTTP/2, clients opening parallel connectionsMany connections per client, each needing a handshakeConnection count vs request count; check if mod_http2 is loaded

Quick checks

All read-only, safe to run on a production box.

# 1. Confirm CPU is the bottleneck and it is Apache consuming it
ps -C httpd -o pid,%cpu --sort=-%cpu 2>/dev/null || ps -C apache2 -o pid,%cpu --sort=-%cpu

# 2. New-connection rate: PassiveOpens in /proc/net/snmp counts accepted
#    TCP connections (all ports). Sample twice and take the delta.
awk '$1=="Tcp:"{print $6}' /proc/net/snmp
sleep 10
awk '$1=="Tcp:"{print $6}' /proc/net/snmp
# Note: the delta of established counts (ss -tn state established) is NOT a
# new-connection rate, because closed connections vanish from the list.

# 3. Test session resumption directly against the server
openssl s_client -connect localhost:443 -reconnect 2>/dev/null | grep -c "Reused"
# Expect 5 (six connections, five reused). 0 means resumption is fully broken.

# 4. Check the configured session cache
grep -rEi 'SSLSessionCache|SSLSessionTickets' /etc/httpd/ /etc/apache2/ 2>/dev/null

# 5. Verify the shmcb provider module is loaded
apachectl -M 2>/dev/null | grep socache

# 6. Look for the cache-provider error and OCSP stapling errors
grep -E "shmcb|AH01929|AH02217" /var/log/apache2/error.log /var/log/httpd/error_log 2>/dev/null | tail

# 7. See what certificate type clients pay for
echo | openssl s_client -connect localhost:443 -servername $(hostname) 2>/dev/null | \
  openssl x509 -noout -text | grep "Public Key Algorithm"

Check 3 is the decisive one. s_client -reconnect establishes six connections on one session and reports how many were reused. If it prints 0, resumption is broken end to end, regardless of what the config says.

How to diagnose it

  1. Correlate CPU with new connections, not requests. Pull CPU and the new-connection rate to :443 for the same window. If CPU tracks connection rate rather than request rate, handshakes are the load. If CPU tracks request rate instead, look at mod_deflate, mod_rewrite, or mod_security first; see Apache CPU saturation.

  2. Measure resumption from the client’s perspective. Run openssl s_client -connect localhost:443 -reconnect and count “Reused” lines. Run it again against the public hostname through the load balancer, not just localhost, because the LB may terminate TLS itself or distribute connections across Apache children.

  3. Determine which resumption mechanism should be working. If SSLSessionTickets on (the default), tickets should resume across children. If tickets are off, resumption depends entirely on the server-side cache, which must be shmcb shared memory. Per-child in-memory caching does not survive process boundaries.

  4. Check the cache configuration and module. Confirm mod_socache_shmcb is loaded (it moved to a separate module in 2.4) and that SSLSessionCache shmcb:/path(size) is set. A missing module on an upgraded config produces SSLSessionCache: 'shmcb' session cache not supported. Loading a newly added module requires a full restart, not a reload.

  5. Test the graceful-restart edge case. If you run TLS 1.3 and recently did apachectl graceful, re-run the -reconnect test. There is a known behavior where TLS 1.3 resumption silently falls back to full handshakes after a graceful restart with session tickets on; TLS 1.2 is unaffected.

  6. Estimate cache sizing. The commonly copied example shmcb:/path/to/ssl_scache(512000) allocates 512 KiB. On a busy server this turns over fast and effective resumption collapses with no error logged. If resumption degrades at peak but works off-peak, cache size is the suspect.

  7. Rule out a post-restart handshake burst. After any crash or hard restart, the session cache is empty and you get a transient burst of full handshakes. That is normal cold-start behavior. It is a problem only if CPU stays high after the cache should have warmed.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
Session resumption rate (cache hit/miss)The direct measure of the problemBelow ~80% on a server with repeat visitors
New-connection rate to :443New connections are potential full handshakesRising without a matching request-rate rise
Apache CPU per coreFull handshakes are the top CPU consumerSustained >80% at moderate traffic
CPU per requestDistinguishes per-connection cost from per-request costRising when connection rate rises, flat when request rate rises
Connections vs requests ratioHTTP/2 multiplexing lowers handshake countMany connections per request (HTTP/1.1 parallel connections)
TTFB on new connectionsHandshake time lands in the first byteElevated TTFB concentrated on new connections
OCSP stapling errors (AH01929, AH02217)Silent stapling failure adds client-side latencyAny occurrence in the error log

Apache exposes no native metric for resumption rate. You infer it from the correlation above, or measure it periodically with openssl s_client -reconnect. That gap is why this failure goes undiagnosed for so long.

Fixes

Enable and size the shared session cache

Load mod_socache_shmcb and configure a shared-memory cache, sized for your concurrent session count rather than the 512 KiB example from the docs:

LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
SSLSessionCache shmcb:/var/run/httpd/ssl_scache(10485760)
SSLSessionCacheTimeout 300

The path differs by distribution. A full restart (not graceful) is required after loading the module, so schedule it: a restart drops in-flight connections and briefly empties the session cache. SSLSessionCacheTimeout defaults to 300 seconds; note the timeout is only checked when a session is presented, so stale entries are never purged in the background. Raise the timeout if clients typically return within a longer window, but size the cache so it does not fill first.

Deal with the TLS 1.3 graceful-restart bug

If resumption dies after every graceful restart and you serve TLS 1.3, the documented workaround is SSLSessionTickets off, which switches OpenSSL to stateful (session ID) resumption backed by the shmcb cache. Tradeoff: tickets are off, so resumption now depends entirely on your shared cache being enabled and sized correctly. Do not set tickets off without configuring shmcb, or resumption gets worse.

Prefer ECDSA certificates

ECDSA P-256 signing is roughly an order of magnitude faster than RSA-2048 per operation, which directly cuts the CPU cost of every full handshake. Measure the gap on your own hardware:

# Compare signing performance on this host
openssl speed rsa2048 ecdsap256

Tradeoff: older clients may lack ECDSA support, so dual RSA+ECDSA certificate deployment is common during transition. Even with resumption fixed, ECDSA shrinks the residual cost of the handshakes that cannot be resumed (first visits, cache misses, post-restart bursts).

Reduce handshake count with HTTP/2

HTTP/2 multiplexes many requests over one connection, which directly reduces the number of handshakes your clients need. Requirements: mod_http2 and the event MPM. Enabling it under mpm_prefork fails with AH10034: The mpm module (prefork.c) is not supported by mod_http2, which usually means migrating from mod_php to PHP-FPM first. This does not fix broken resumption, but it lowers the connection rate, which lowers the price of any resumption gap.

Rotate ticket keys deliberately

Apache generates one random ticket key at startup and keeps it for the life of the process. There is no automatic rotation, which weakens forward secrecy for resumed sessions. The practical approaches are periodic restarts (daily, via cron) or SSLSessionTicketKeyFile pointing at a 48-byte random key file that you replace on a schedule, with a restart to pick up the new key. Key rotation via restart empties the shmcb cache as a side effect, so expect a brief full-handshake burst afterward.

Prevention

  • Set SSLSessionCache explicitly. The default is none. Any HTTPS server with repeat clients should have a sized shmcb cache in the base config.
  • Alert on resumption rate. A periodic openssl s_client -reconnect probe from an external host catches regressions that no log line ever will.
  • Graph CPU against new-connection rate. The divergence between request-driven and connection-driven CPU is the earliest warning.
  • Test resumption in deploy pipelines. Run the -reconnect probe after every config change or restart that touches mod_ssl, including graceful restarts.
  • Keep Apache current. TLS session handling has had real security fixes, including CVE-2025-23048 (access control bypass via session resumption, fixed in 2.4.64) and CVE-2024-47252 (mod_ssl log injection). Resumption bugs are not only a performance concern.

How Netdata helps

  • Per-second CPU per process, so handshake-driven load is visible as a sharp ramp correlated with traffic events rather than a five-minute average that hides it.
  • New-connection and established-connection counts on :443, letting you overlay connection rate against CPU and see immediately whether load tracks connections or requests.
  • Connections-per-request visibility that shows whether HTTP/2 multiplexing is actually reducing connection churn.
  • Certificate expiry monitoring, so the certificate side of TLS (including renewals that restart Apache and empty the session cache) is watched alongside performance.
  • Restart and uptime tracking, which explains post-restart full-handshake bursts when the cache empties.
  • Correlating these signals on one dashboard shortens the path from “CPU is high” to “resumption rate collapsed at 14:02, right after the graceful restart.”

Netdata’s Apache HTTP Server monitoring with Netdata brings these signals together with per-second metrics and ML anomaly detection.