When managing production web servers (Apache, PHP-FPM, and Nginx), routine application events often involve detailed mechanics across lower operating system layers.
Recently, I investigated how LiveZilla handles session teardowns and active connection terminations. While user interfaces present a simple "close chat" button, the underlying lifecycle triggers PHP handlers, MySQL transactions, Linux kernel socket state transitions, and file descriptor management.
Below is an analysis of the system calls (syscalls), memory and I/O implications, and practical commands to diagnose lingering ghost sockets.
1. How session termination operates in practice#
In LiveZilla, sessions terminate through three main pathways:
A. Explicit operator or visitor command#
- The client browser dispatches a
POSTrequest toserver.phpwith action flags (such asp_act=close_chat). - PHP processes the request and executes an
UPDATEin MySQL (lz_chat_postsandlz_chat_sessions), marking the chat as closed. - In Long Polling mode, the pending polling request is answered and client-side JavaScript loops are halted.
B. Inactivity timeout (logical garbage collection)#
When a visitor abruptly closes the browser tab:
- The LiveZilla server applies an inactivity timeout (
Configuration -> Server -> Timeout). - Because PHP is stateless, the process that served the last request has already exited. The database record remains active until a subsequent maintenance routine validates timestamps and marks the session dead.
C. WebSockets transport layer#
When operating over WebSockets:
- The client sends a
CLOSEframe (Opcode0x8). - The server worker executes a
close()syscall on the corresponding socket file descriptor.
2. I/o footprint, filesystem, and session storage#
During conversation termination, several storage operations occur:
- Transcript persistence: Message buffers are serialized, moved to history tables, or dispatched via email. If debug logging is enabled, synchronous
fopen()andfwrite()calls execute. - PHP session cleanup and inodes: PHP's
session_destroy()unlinks temporary session files (sess_*). If PHP session garbage collection (session.gc_probability) is misconfigured, unpurged legacy files can exhaust filesystem inodes.
3. Investigating hung connections in CLOSE_WAIT#
A frequent production anomaly occurs when the application marks a session closed, yet the underlying TCP connection remains in CLOSE_WAIT.
Why does CLOSE_WAIT occur?#
When the remote client closes a connection, its kernel transmits a FIN packet. The Linux server kernel responds with an ACK and places the local socket in CLOSE_WAIT. From this moment, the kernel waits for the application process (Apache or PHP-FPM) to issue an explicit close() syscall.
If PHP hangs on a database lock or exceeds execution timeouts, the socket remains open, consuming server worker connection slots.
Terminal diagnostic commands#
To inspect sockets stuck in CLOSE_WAIT on web ports idiomatically:
ss -tanp state close-wait '( sport = :http or sport = :https )'
To list open network descriptors for a specific remote IP (capturing Apache, PHP-FPM, or Nginx workers alike):
lsof -nPi | grep 192.0.2.10
To trace real-time system calls on a suspected PHP-FPM worker:
strace -p <PHP_FPM_PID> -e trace=network,file,desc
The strace output reveals the complete sequence: recvfrom() (reading the close payload), sendto() (persisting changes to MySQL), and the final close() releasing the socket descriptor.
4. Webserver and kernel tuning guidelines#
Two common edge cases require attention:
- MySQL metadata locks: If session tables lock during heavy queries, the PHP process remains blocked waiting on a network socket (interruptible sleep,
S state) until MySQL resolves the lock — preventing PHP from callingclose()on the visitor's socket or serving new requests. - Reverse proxies and CDNs (Cloudflare): The logical chat session may be terminated, but the TCP tunnel between the CDN edge and origin server remains open due to HTTP Keep-Alive reuse.
Recommended configurations:#
A. Application-level fixes (the only real remedy for CLOSE_WAIT)#
Because the Linux kernel does not implement an automatic timeout for sockets in CLOSE_WAIT, clearing them requires the userland application to invoke close():
- PHP-FPM: Configure
request_terminate_timeout = 30sin your PHP-FPM pool to terminate scripts hanging on database locks and release the socket. If a worker hangs indefinitely, terminating the process (kill -9 <PID>) allows the FPM manager to spawn a clean worker while the kernel tears down inherited sockets. - Apache / Nginx: Reduce
KeepAliveTimeoutto2–5seconds in the web server to release idle sockets promptly, keeping request timeouts aligned with application workloads.
B. Kernel TCP tuning (sysctl)#
# /etc/sysctl.d/99-tcp-tuning.conf
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_fin_timeout = 30
Apply immediately with sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf.
Tracing these lifecycles using ss, lsof, and strace ensures applications terminate sessions cleanly without leaking socket descriptors or memory.
Conclusion#
In this article, I analyzed the complete lifecycle of a LiveZilla session termination — from MySQL updates to socket-level close() calls — demonstrating that persistent CLOSE_WAIT sockets are almost never a kernel bug, but an application failing to release descriptors. With well-calibrated PHP-FPM timeouts and active socket inspection, web servers maintain healthy worker pools under heavy concurrent workloads.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments