• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Fixing Out of Memory (OOM) Killer terminating PHP-FPM pool workers in Legacy WordPress Codebases Without Breaking API Contracts

Fixing Out of Memory (OOM) Killer terminating PHP-FPM pool workers in Legacy WordPress Codebases Without Breaking API Contracts

Diagnosing OOM Killer Invocation on PHP-FPM Workers

The Linux Out-Of-Memory (OOM) Killer is a critical system process designed to reclaim memory when the system is under severe pressure. While essential for preventing a full system hang, its indiscriminate targeting of processes, particularly PHP-FPM worker pools, can lead to intermittent and frustrating application failures. In legacy WordPress codebases, which often lack modern memory management practices and can accumulate bloat over time, this issue is particularly prevalent. Identifying the root cause requires a systematic approach, starting with kernel-level diagnostics.

The first step is to confirm that the OOM Killer is indeed the culprit. Kernel logs are the definitive source for this information. We can use `dmesg` or query the system journal for relevant messages.

Leveraging Kernel Logs for OOM Identification

Execute the following command to inspect kernel messages for OOM events. Look for lines containing “Out of memory” and identify the process that was terminated. Often, you’ll see the `php-fpm` process or its child worker processes (e.g., `php-fpm: pool www`).

sudo dmesg -T | grep -i "out of memory"
# Or using journalctl for systemd-based systems
sudo journalctl -k | grep -i "out of memory"

A typical OOM killer log entry might look like this:

[Tue Aug 15 10:30:00 2023] Out of memory: Kill process 12345 (php-fpm) score 987 or sacrifice child
[Tue Aug 15 10:30:00 2023] Killed process 12345 (php-fpm) total-vm:123456kB, anon-rss:65432kB, file-rss:1024kB

The `score` indicates the OOM killer’s perceived “badness” of a process. Higher scores mean a process is more likely to be terminated. Factors influencing this score include memory usage, process age, and niceness. The `total-vm` and `anon-rss` (anonymous resident set size) are key indicators of the memory consumed by the process.

Profiling PHP-FPM Worker Memory Consumption

Once confirmed, the next step is to understand *why* the PHP-FPM workers are consuming excessive memory. Legacy WordPress sites often suffer from unoptimized database queries, inefficient plugin code, large image processing, or memory leaks within custom code. We need to profile individual requests that are likely to trigger OOM conditions.

Enabling PHP SlowLog and Memory Profiling

PHP-FPM’s built-in slow log can be configured to log requests that exceed a certain execution time. While not directly a memory profiler, long-running requests are often correlated with high memory usage. More importantly, we can enable PHP’s built-in memory profiling capabilities.

Edit your `php.ini` or a dedicated FPM pool configuration file (e.g., `/etc/php/8.1/fpm/pool.d/www.conf`) to enable the following directives. Restart PHP-FPM after changes.

; Enable slow log to identify long-running requests
request_slowlog_timeout = 30 ; Log requests longer than 30 seconds
slowlog = /var/log/php/php-fpm-slow.log

; Enable memory limit for individual scripts (though OOM killer operates at OS level)
memory_limit = 256M ; Adjust as needed, but this is a script limit, not OS limit

; Enable profiling (requires Xdebug or similar, but can also use basic PHP functions)
; For more advanced profiling, consider Xdebug or Blackfire.io
; For basic memory tracking, we'll use custom code.

Restart PHP-FPM:

sudo systemctl restart php8.1-fpm

Custom Memory Tracking in WordPress Hooks

To pinpoint memory-hungry parts of your WordPress application, we can instrument critical hooks with custom memory tracking. This involves hooking into actions and filters that are commonly executed during page loads or AJAX requests.

Add the following code to your theme’s `functions.php` file or a custom plugin. This code will log the memory usage at various stages of the WordPress execution lifecycle. Be cautious with this in production; it’s primarily for debugging. Consider conditional loading based on environment variables or specific user agents.

// Add this to your theme's functions.php or a custom plugin

// Global variable to store memory usage snapshots
$GLOBALS['memory_usage_log'] = [];

function log_memory_usage(string $context = 'general') {
    if (!defined('WP_DEBUG') || !WP_DEBUG) {
        // Only log in debug mode to avoid performance impact
        return;
    }

    // Get current memory usage in MB
    $memory_used = memory_get_usage(true) / 1024 / 1024; // Real memory usage

    $GLOBALS['memory_usage_log'][] = [
        'context' => $context,
        'memory_mb' => round($memory_used, 2),
        'timestamp' => microtime(true),
        'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5) // Capture call stack
    ];
}

// Hook into early WordPress execution
add_action('plugins_loaded', function() { log_memory_usage('plugins_loaded'); });
add_action('after_setup_theme', function() { log_memory_usage('after_setup_theme'); });
add_action('init', function() { log_memory_usage('init'); });
add_action('wp_loaded', function() { log_memory_usage('wp_loaded'); });

// Hook into template loading
add_action('template_redirect', function() { log_memory_usage('template_redirect'); });

// Hook into query execution
add_action('parse_query', function() { log_memory_usage('parse_query'); });
add_action('posts_selection', function() { log_memory_usage('posts_selection'); }); // After SQL query is built

// Hook into rendering
add_action('wp_head', function() { log_memory_usage('wp_head'); });
add_action('wp_footer', function() { log_memory_usage('wp_footer'); });

// Hook for AJAX requests
add_action('wp_ajax_nopriv_my_ajax_action', function() { log_memory_usage('ajax_nopriv_start'); });
add_action('wp_ajax_my_ajax_action', function() { log_memory_usage('ajax_start'); });

// Hook for AJAX responses (example)
add_action('wp_ajax_my_ajax_action', function() {
    log_memory_usage('ajax_processing_end');
    // ... AJAX logic ...
    wp_send_json_success(); // Or wp_send_json_error()
}, 999); // High priority to run after most AJAX logic

// Finalize and log memory usage before output
add_action('shutdown', function() {
    if (!defined('WP_DEBUG') || !WP_DEBUG) {
        return;
    }

    log_memory_usage('shutdown');

    if (empty($GLOBALS['memory_usage_log'])) {
        return;
    }

    $output = "

WordPress Memory Usage Log

\n"; $output .= "
\n";
    $output .= "Context | Memory (MB) | Timestamp | Call Stack\n";
    $output .= "--------------------------------------------------\n";

    $previous_memory = 0;
    foreach ($GLOBALS['memory_usage_log'] as $log_entry) {
        $memory_diff = $log_entry['memory_mb'] - $previous_memory;
        $output .= sprintf(
            "%s | %.2f MB (+%.2f MB) | %.4f | %s\n",
            $log_entry['context'],
            $log_entry['memory_mb'],
            $memory_diff,
            $log_entry['timestamp'],
            implode(' > ', array_map(function($call) {
                return (isset($call['class']) ? $call['class'] . $call['type'] : '') . $call['function'] . '()';
            }, $log_entry['backtrace']))
        );
        $previous_memory = $log_entry['memory_mb'];
    }
    $output .= "
\n"; // Output this log only if WP_DEBUG is true and not in an AJAX response context if (defined('WP_DEBUG') && WP_DEBUG && !wp_doing_ajax()) { echo $output; } // Optionally, log to a file for non-debug environments or persistent tracking // error_log(print_r($GLOBALS['memory_usage_log'], true), 3, '/var/log/wordpress/memory_debug.log'); });

When a page loads, this script will output a table detailing memory usage at different WordPress execution points. Analyze this output to identify which hook or phase of execution is causing significant memory spikes. The `backtrace` will help pinpoint the specific functions or plugins contributing to the increase.

Optimizing PHP-FPM Configuration for Memory Management

Beyond application-level fixes, PHP-FPM’s own configuration plays a crucial role in managing worker memory. The OOM Killer is a last resort; proper FPM tuning can prevent reaching that point.

Tuning PHP-FPM Process Manager Settings

The `pm` (process manager) settings in your PHP-FPM pool configuration are critical. For legacy systems, `pm = dynamic` is common, but `pm = ondemand` or careful tuning of `pm.max_children`, `pm.start_servers`, `pm.min_spare_servers`, and `pm.max_spare_servers` can be more effective.

Consider the following configuration in `/etc/php/8.1/fpm/pool.d/www.conf`:

; Process Manager settings
pm = dynamic
; pm.max_children: The maximum number of child processes that will be spawned.
; This is the most critical setting. Set it based on available RAM and typical worker memory usage.
; Example: If each worker uses ~50MB and you have 4GB RAM, you might aim for max_children around 80 (4096MB / 50MB).
; Leave room for the OS and other services.
pm.max_children = 80

; pm.start_servers: Number of child processes to start when PHP-FPM starts.
pm.start_servers = 10

; pm.min_spare_servers: Minimum number of idle (spare) processes.
pm.min_spare_servers = 5

; pm.max_spare_servers: Maximum number of idle (spare) processes.
pm.max_spare_servers = 20

; pm.max_requests: The number of requests each child process should execute before respawning.
; This helps to clear memory leaks in long-running processes. Set to a reasonable number.
pm.max_requests = 500

; If using 'ondemand' process manager, it starts workers only when needed.
; pm = ondemand
; pm.max_children = 100 ; Still need to define max_children
; pm.process_idle_timeout = 10s ; Timeout for idle processes to be killed

Key considerations for `pm.max_children`:

  • Calculate the average memory footprint of a single PHP-FPM worker process under load. Use `ps aux | grep php-fpm` and observe the `RSS` (Resident Set Size) column for worker processes.
  • Divide your total available RAM (minus OS and other critical service needs) by the average worker memory footprint.
  • Set `pm.max_children` to a value slightly lower than this calculation to provide a buffer.
  • Monitor system load and OOM events after adjusting.

System-Level Memory Tuning

Sometimes, the issue isn’t just PHP-FPM but the system’s overall memory management. Adjusting the OOM killer’s behavior can be a last resort, but it’s important to understand its parameters.

The `oom_score_adj` value for a process influences its likelihood of being killed. A value of `-1000` disables the OOM killer for that process, while `1000` makes it highly likely to be killed. We can adjust this for `php-fpm` processes, but this is generally discouraged as it can lead to system instability if memory is truly exhausted.

# Check current oom_score_adj for PHP-FPM processes
sudo find /proc -maxdepth 1 -type d -regex '/proc/[0-9]+' -exec grep -l "php-fpm" {}/cmdline \; -exec cat {}/oom_score_adj \;

# Example: To make php-fpm less likely to be killed (use with extreme caution)
# Find the PID of the master php-fpm process or a worker
PHP_FPM_PID=$(pgrep -f "php-fpm: master process")
echo -100 | sudo tee /proc/$PHP_FPM_PID/oom_score_adj

A more robust approach is to configure memory limits at the cgroup level if you are using systemd or Docker. This provides more granular control over resource allocation.

Refactoring Legacy WordPress Code for Memory Efficiency

The most sustainable solution involves refactoring the legacy WordPress codebase to be more memory-efficient. This is a strategic refactoring effort that pays dividends in stability and performance.

Database Query Optimization

Inefficient database queries are a primary cause of high memory usage in WordPress. This often stems from plugins that perform redundant queries, fetch excessive data, or use `WP_Query` incorrectly.

Common Pitfalls and Solutions:

  • N+1 Query Problem: Ensure that loops fetching posts or meta data do not execute a separate database query for each item. Use `WP_Query`’s `fields` parameter to select only necessary columns, or use `JOIN`s where appropriate.
  • Fetching Unnecessary Data: Instead of `get_posts()` or `WP_Query` with default arguments, specify `fields` like `’ids’` or `’id=>parent’` when only IDs are needed.
  • Caching: Implement object caching (e.g., Redis, Memcached) for frequently accessed data, especially complex query results. WordPress’s Transients API can also be leveraged.
  • `get_posts()` vs. `WP_Query`: While `get_posts()` is a wrapper for `WP_Query`, understand its limitations. For complex scenarios, direct `WP_Query` offers more control.

Example of optimizing a query to fetch only post IDs:

// Inefficient: Fetches full post objects
$posts = get_posts( array(
    'numberposts' => -1,
    'post_type'   => 'product',
    'post_status' => 'publish',
) );

// Efficient: Fetches only IDs
$post_ids = get_posts( array(
    'numberposts' => -1,
    'post_type'   => 'product',
    'post_status' => 'publish',
    'fields'      => 'ids', // Crucial for memory efficiency
) );

// If you need to process these IDs, you can then fetch them in batches or use them directly.
// For very large numbers of IDs, consider fetching them in batches to avoid memory spikes.

Plugin and Theme Code Audits

Legacy plugins and themes are often the source of memory leaks or inefficient resource usage. Conduct regular audits:

  • Deactivate Unused Plugins: Remove any plugins that are not actively used.
  • Memory Leaks: Look for code that continuously appends to global arrays or objects without proper cleanup, especially within long-running processes or AJAX handlers.
  • Resource-Intensive Operations: Identify plugins that perform heavy image manipulation, complex data processing, or extensive logging without proper batching or asynchronous handling.
  • Update Dependencies: Ensure all plugins and themes are up-to-date, as updates often include performance and memory optimizations.

When developing new features or refactoring existing ones, adhere to WordPress coding standards and best practices for memory management. This includes using appropriate WordPress APIs, avoiding global state where possible, and cleaning up resources.

Conclusion: A Multi-faceted Approach

Fixing OOM Killer terminations of PHP-FPM workers in legacy WordPress codebases is rarely a single-fix solution. It requires a combination of diligent system-level diagnostics, application-level profiling, PHP-FPM configuration tuning, and strategic code refactoring. By systematically identifying the root cause—whether it’s a specific plugin, a poorly optimized query, or insufficient server resources—and addressing it with targeted optimizations, you can significantly improve the stability and performance of your WordPress application.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala