
If you’ve ever pulled up your server’s access log and found admin-ajax.php hit thousands of times an hour, you’ve met one of WordPress’s most useful and most abused endpoints. It’s the front door for a huge range of plugin features, and when something goes wrong, it becomes the single busiest URL on the site, often with none of it cacheable by default.
Why admin-ajax.php Gets Hammered
admin-ajax.php is WordPress’s generic handler for asynchronous requests. Any plugin can register an action against it, on the logged-in side or the public side, and WordPress will route the request through a full bootstrap: loading core, all active plugins, and the theme, before the specific handler ever runs. That’s expensive compared to a static asset or even a cached page load, and it happens on every single call because the endpoint is, by design, dynamic.
The problems start when a plugin calls it too often, for too many visitors, or for work that didn’t need a live PHP process at all. A cart fragment refresh every few seconds, a stats widget polling on every page view, or a heartbeat check running on every admin tab left open in a browser can add up to real load, especially on PHP-FPM workers or database connections that are also serving actual page requests.
Finding the Culprit
Start with evidence, not guesses. A quick grep of your access log will tell you how much traffic is actually landing on the endpoint:
grep "admin-ajax.php" /var/log/nginx/access.log | wc -l
grep "admin-ajax.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
The first command gives you a raw count over the log window. The second groups by IP so you can see whether the volume is coming from real visitors, a single aggressive bot, or your own logged-in users’ browsers idling with multiple tabs open.
Next, identify which action is actually running. Every admin-ajax request carries an action parameter that maps to a specific plugin hook, so filtering for it in the log (if your log format captures query strings or POST bodies) or watching it live is the fastest way to name the offender:
tail -f /var/log/nginx/access.log | grep "admin-ajax.php"
On the WordPress side, the Query Monitor plugin is the most direct tool. Install it temporarily on a staging copy of the site, trigger the behavior, and check the AJAX tab, it will show you the action name, the callback function, and which plugin registered it. Browser devtools work too: open the Network tab, filter for admin-ajax.php, and watch which page interactions fire it and how often.
If you suspect a background process rather than a live visitor, wp-cli can confirm whether wp-cron is somehow tangled up in it:
wp cron event list --fields=hook,next_run_relative
wp plugin list --status=active
Cross-reference the active plugin list against known heavy hitters. WooCommerce’s cart fragments, caching plugins’ own AJAX-based cache warmers, form builders with live validation, and analytics or heatmap plugins that log every scroll event are the usual suspects.
The Heartbeat API: A Frequent Repeat Offender
WordPress’s built-in Heartbeat API uses admin-ajax to check for post locks, autosave, and session state in the dashboard, and it can fire every 15 to 60 seconds per open browser tab. On a site with several editors logged in throughout the day, that alone can generate a steady background load. You can throttle or disable it without breaking core functionality:
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 60;
return $settings;
} );
If autosave and post locking aren’t critical for your workflow, some sites disable Heartbeat entirely on the front end and in the post editor screens, keeping it only where a plugin genuinely depends on it (some page builders and WooCommerce order screens do).
Moving Work Off admin-ajax
Once you know what’s calling the endpoint, the fix is usually to move the work somewhere more appropriate rather than trying to cache a fundamentally dynamic request.
- REST API: If a plugin developer controls the code, a custom REST route is lighter than admin-ajax because it skips some of the legacy overhead and integrates with WordPress’s native authentication and rate-limiting hooks more cleanly. If you’re evaluating plugins, ones built on the REST API tend to behave better under load than ones still using admin-ajax for everything.
- wp-cron for scheduled work: Polling behavior (checking for new orders, refreshing a cache, syncing an external feed) belongs in a scheduled cron event, not a JavaScript timer hitting admin-ajax. If a plugin insists on doing this via AJAX polling, check its settings for a “use cron instead” option, or disable the polling feature and rely on WordPress’s own cron schedule (ideally driven by a real system cron job rather than the default page-load trigger, which is more reliable on lower-traffic sites too).
- admin-post.php for one-off form submissions: If the traffic is coming from a contact form or similar single-submit action, admin-post.php is a more appropriate handler than admin-ajax for a full-page-reload workflow, and it avoids the JavaScript polling overhead entirely.
Caching Around What’s Left
Some admin-ajax traffic is legitimate and can’t be eliminated, but it can often be cached or offloaded. LiteSpeed Cache lets you configure specific admin-ajax actions to be treated as cacheable at the edge or the server, useful for things like a search-suggestions endpoint that returns the same result for the same query. Where the response genuinely needs to be dynamic per user but expensive to compute, a Redis object cache drop-in reduces the database load behind the scenes so each admin-ajax call becomes cheaper even if the request volume doesn’t change.
Cloudflare sits in front of all of this and can help with the abusive end of the traffic. If the flood is coming from bots rather than real users, a WAF rule or rate limit on the admin-ajax.php path specifically (scoped by the action parameter if your plan supports it) stops the requests before they ever reach your origin. This is one of those cases where infrastructure-level handling matters: a managed host running LiteSpeed and Redis together, which is how we set things up at ServerBorn, absorbs a good chunk of this load automatically, but knowing which plugin is causing it still saves you from a bigger fix down the line.
Excess admin-ajax traffic also affects Core Web Vitals, particularly INP, since a slow or frequent AJAX call can block the main thread on interaction. Fixing the root cause pays off in both server load and real user experience.
The Takeaway
admin-ajax.php overload is almost always traceable to one specific plugin action once you look. Grep your logs, confirm with Query Monitor or devtools, then decide whether the work belongs on a schedule, on the REST API, or nowhere at all. Cache and rate-limit what’s left, and the endpoint stops being your busiest problem and goes back to being just another part of WordPress doing its job quietly.