Eliminating PHP-FPM Bottlenecks in Handyman Service Sites

Tuning TCP Stack for Handz Booking Theme Latency

The deployment of the Handz – Handyman & Plumber Repair WordPress Theme on a cluster of Rocky Linux 9 nodes revealed an edge-case latency issue that standard application profiling failed to capture. The stack involves Nginx 1.26, PHP 8.3-FPM, and MariaDB 11.4. During a scheduled maintenance window, while verifying the integrity of the "Instant Quote" calculator—a core feature of the Handz theme—we observed the 99th percentile response time for admin-ajax.php drifting from 150ms to nearly 900ms. This occurred despite the CPU load remaining under 5% and no indications of I/O wait on the NVMe arrays.

The Diagnostic Path: Socket State Analysis and PHP-FPM Status

Primary investigation avoided the usual suspects. I bypassed application-level debugging and went straight to the transport layer. Using netstat -nat | awk '{print $6}' | sort | uniq -c | sort -n, I identified over 14,000 sockets in the TIME_WAIT state. On a low-traffic service site, this indicates a failure in connection recycling between the Nginx reverse proxy and the PHP-FPM upstream. The Handz theme triggers an AJAX request for every character entered into its zip-code validation field. If Nginx is not configured for keepalive connections to the FastCGI backend, every single keystroke creates, uses, and tears down a TCP connection.

I examined the PHP-FPM status page by enabling the pm.status_path in the pool configuration. The "slow requests" counter was incrementing, but the request_slowlog_timeout showed that the delays were happening before the script execution reached the theme's core logic. The bottleneck was at the accept() queue. When the kernel's net.core.somaxconn is set to the default 128, and the PHP-FPM listen.backlog is similarly restricted, the system begins to drop or delay new connection attempts when the socket churn is high.

Kernel Network Stack and TCP Reuse

To rectify the socket exhaustion, I delved into the /etc/sysctl.conf parameters. The immediate requirement was to enable net.ipv4.tcp_tw_reuse. Unlike the deprecated and dangerous tcp_tw_recycle, tcp_tw_reuse allows the kernel to utilize a socket in TIME_WAIT for a new connection if it is deemed safe from a protocol standpoint. We also reduced the net.ipv4.tcp_fin_timeout from the default 60 seconds to 15 seconds. This ensures that sockets held in the FIN-WAIT-2 state are released more aggressively back into the pool.

For site owners looking to Download WooCommerce Theme packages for specialized trades like plumbing, the focus is often on the CSS or the block editor compatibility. However, the Handz theme's quote engine relies on a specific sequence of PHP hooks that are highly sensitive to the max_execution_time and memory_limit of the worker. In our trace, we found that the theme enqueues twelve different JavaScript files for the calculator, each making a pre-flight check to the backend.

Database Indexing in the Handz Service Tables

The Handz theme creates several custom tables to manage plumber availability and service areas. Using the MariaDB slow_query_log with long_query_time = 0.05, we identified a specific query targeting the wp_handz_service_areas table. The query used a LIKE operator on a non-indexed column for zip-code ranges. While the dataset was small—roughly 2,000 rows—the frequency of the calls during the AJAX typing events caused the InnoDB buffer pool to thrash.

I implemented a composite index on the zip_code and service_status columns. This transitioned the query from a full table scan to an index seek. The execution time dropped from 42ms to 0.8ms. In a high-frequency AJAX environment, a 40ms savings per request is the difference between a fluid UI and a stuttering one.

PHP-FPM Process Management: Moving to Static

The default pm = dynamic setting in PHP-FPM is often suboptimal for dedicated service environments. In the dynamic mode, the parent process constantly forks and kills children based on demand. For the Handz theme, the overhead of forking a new worker to handle a 10kb AJAX payload is wasteful. I transitioned the pool to pm = static, setting pm.max_children to 64. This allocates a fixed amount of memory but ensures that a worker is always warm and ready to accept() the incoming fastcgi packet.

Furthermore, we examined the opcache.revalidate_freq. For production, we set this to 0 (or disabled it by ensuring opcache.validate_timestamps=0). This prevents the engine from checking the disk for file changes on every request. Since the Handz theme files are static once deployed, this saves thousands of stat() calls per hour, further reducing the kernel's filesystem overhead.

Nginx Upstream Keepalives

The final piece of the latency puzzle was the connection between Nginx and PHP-FPM. By default, Nginx uses a new connection for every request to an upstream. I modified the Nginx configuration to use an upstream block with a keepalive directive.

This requires Nginx to communicate with PHP-FPM over a TCP socket rather than a Unix domain socket, or to use the newer fastcgi_keep_conn on; directive which allows the FastCGI connection to remain open. We opted for the latter. By maintaining a pool of open connections to the PHP-FPM workers, we eliminated the TCP handshake and the subsequent TIME_WAIT state entirely for the majority of the quote-engine traffic.

Object Caching for Service Availability

The Handz theme frequently queries the wp_options table for its internal configuration settings. By default, WordPress caches these in memory for the duration of a single request. However, with the theme's high-frequency AJAX pattern, these options are re-read from the database on every keystroke. I integrated Redis as an external object cache.

With Redis, the "Service Area" settings and "Plumber Availability" slots are stored in-memory across requests. The get_option calls now resolve in microseconds without hitting the MariaDB service. We observed a 30% reduction in the us (user) time in top after the Redis migration.

Fine-Tuning the Handz AJAX Handlers

A specific technical debt was found in the Handz theme's functions.php. The AJAX handler for the "Service Quote" was loading the entire WooCommerce core to calculate a simple tax rate. For a handyman site, this is overkill. I refactored the handler to use a light-weight tax calculation logic that only pulls the necessary rate from a flat file if the WooCommerce environment isn't strictly required for the initial quote display.

This change alone reduced the memory footprint of the AJAX worker from 64MB to 22MB. In a pm = static configuration with 64 children, this saved over 2GB of RAM, which was then re-allocated to the MariaDB innodb_buffer_pool_size.

Kernel Backlog and Somaxconn

To ensure the system handles spikes in the plumbing booking requests, I increased the system-wide listener limits. If the Handz theme is used during a local promotion, the number of concurrent connections can momentarily exceed the default thresholds.

I set net.core.somaxconn = 4096 and net.ipv4.tcp_max_syn_backlog = 8192. These changes allow the kernel to maintain a larger queue of "half-open" and "ready" connections. This prevents the "Connection Refused" errors that can occur when the PHP-FPM workers are momentarily saturated by long-running image uploads or report generations in the WordPress admin dashboard.

Final Configuration and Snippet

The interaction between the Linux kernel, the Nginx buffer sizes, and the PHP process manager must be harmonious. For the Handz theme, we specifically tuned the fastcgi_buffers to ensure that larger booking summaries do not spill over to disk, which would introduce latency via I/O wait.

# Add to your Nginx server block for Handz theme optimization
upstream php-fpm {
    server 127.0.0.1:9000;
    keepalive 32;
}

location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    fastcgi_pass php-fpm;
    fastcgi_index index.php;
    fastcgi_keep_conn on;
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Ensure your /etc/sysctl.conf includes:

net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192

Do not rely on default WordPress cron for handyman booking reminders; offload this to the system crontab using wp-cli to avoid blocking web workers.

评论 0