A wall of POST /wp-admin/admin-ajax.php in your access log, or a spike of it in a security plugin's report, is almost always your own site, not an attacker. WordPress and its plugins use that one file as the front-door for a lot of their background requests: the editor's post-lock heartbeat, page builders, contact forms, "load more" buttons, analytics pings, and WooCommerce's cart widget. (Modern WordPress also leans on the REST API, so admin-ajax.php is no longer the only AJAX path, but it is still where most plugin traffic lands.) The traffic looks alarming because it is high-volume and all aimed at a single endpoint, which is exactly what a Layer-7 flood looks like too.
So before you block anything: the count alone tells you almost nothing. What tells you whether it is normal or an attack is the action parameter on each request. This article shows how to read it, the handful of cases where admin-ajax.php genuinely is the attack surface, and why the most common "fix", denying the file in .htaccess or a firewall, quietly breaks your editor and every plugin that depends on it.
Where the traffic comes from: Heartbeat and friends
admin-ajax.php is not a bug or a backdoor. It is the documented WordPress way to do server-side AJAX. The plugin handbook tells every developer to send AJAX to wp-admin/admin-ajax.php with an action parameter, which WordPress dispatches to a wp_ajax_{action} hook (for logged-in users) or a wp_ajax_nopriv_{action} hook (for everyone else). That is why traffic from completely unrelated plugins all converges on the same URL.
The single biggest source on most sites is the Heartbeat API, added in WordPress 3.6 (August 2013). Heartbeat POSTs to admin-ajax.php with action=heartbeat on a timer, and the default cadence trips people up:
- In the post editor: every 15 seconds. This is the post-lock refresh (
wp-refresh-post-lock) that powers the "another user is editing this post" warning, not autosave. Autosave to the server has its own interval,AUTOSAVE_INTERVAL, which defaults to 60 seconds. - On the dashboard and other admin screens: every 60 seconds.
- On the front end: off by default. Core Heartbeat does not run for logged-out visitors unless a plugin or theme explicitly enqueues it.
That last point matters: if your front-end shows heavy admin-ajax.php traffic, core Heartbeat is not the cause. A plugin is. Leave the editor open in a background tab on a busy admin and Heartbeat alone can be a few requests a minute per logged-in user, which adds up fast across a team.
The other notorious source is WooCommerce cart fragments. The wc-cart-fragments.js script refreshes the mini-cart on page load by calling get_refreshed_fragments. Older WooCommerce routed that through admin-ajax.php; WooCommerce 2.4 (2015) moved it to a dedicated ?wc-ajax=get_refreshed_fragments endpoint to avoid loading all of wp-admin on every request, and WooCommerce 7.8 (2023) stopped enqueuing the script on pages without a cart widget. On any store between those versions, an uncached POST fires on most front-end pageviews. Same conceptual problem as Heartbeat, different endpoint name in the log.
Add page builders (Elementor, Divi), form plugins, infinite scroll, and stats plugins, and a normal, healthy WordPress site generates a steady stream of admin-ajax.php hits all day. None of it is an attack.
Find the culprit: read the action parameter
The count is noise. The action value is the signal. The catch is that a standard access log only records the request line (POST /wp-admin/admin-ajax.php HTTP/1.1), and the action rides in the POST body, so the log shows a wall of identical lines with nothing to tell them apart.
The reliable way to see what is firing is the browser. Open DevTools, go to the Network tab, filter to admin-ajax.php, and watch. Each request's payload carries the action:

Once you can see the actions, the diagnosis is usually obvious:
action=heartbeaton a regular cadence, on admin screens, by logged-in users: core Heartbeat. Normal.action=get_refreshed_fragments(or?wc-ajax=get_refreshed_fragments) on every front-end page: WooCommerce cart fragments. Normal, often tunable.action=elementor_*,action=*_load_more, a form plugin's action: that plugin doing its job. Normal.
To sanity-check the volume rather than just the action: one editor left open polls every 15 seconds, which is roughly 240 hits an hour from that single tab. A few editors plus cart fragments on a busy store reach into the thousands a day, and that is still normal. The raw count almost never tells you anything on its own. And when the action name is generic, the payload often names the culprit anyway: nonce fields and parameters frequently carry the plugin's slug (a fusion-* nonce points at Avada, for instance), so expand the request body, not just the action column.
Some managed hosts log the action separately so you can rank it from the shell. WP Engine, for example, keeps a dedicated admin-ajax log, and the one-liner there is the pattern worth knowing in general:
grep "action" admin-ajax.log | sort | uniq -c | sort -rn | headThe plugin Query Monitor will also surface AJAX calls with their actions if you would rather not leave the browser. Whichever route, the goal is the same: turn "lots of admin-ajax.php" into "lots of this specific action", because that is what you can actually act on.
Is it ever a real attack?
Yes, and this is the part the "it's always just Heartbeat, ignore it" advice gets wrong. admin-ajax.php is a genuine attack surface in two ways.
The serious one is unauthenticated actions in vulnerable plugins. A wp_ajax_nopriv_{action} hook is public by design: registering it exposes that action to anyone on the internet through admin-ajax.php. That is fine until the handler does something sensitive without checking the caller's capabilities. (A nonce guards against cross-site request forgery; it is not an access-control check and does not make a nopriv action private.) This is one of the most common shapes of WordPress vulnerability, and new ones ship regularly: unauthenticated settings changes, privilege escalation, and SQL injection have all reached production through a nopriv handler that ran a privileged operation without verifying who was asking. If your logs show a flood of one unusual action, especially from logged-out requests across many IPs, that is worth investigating as an attack rather than dismissing.
The cruder one is Layer-7 flooding. Because admin-ajax.php exists on every WordPress install and bootstraps the full stack on each call, it is a convenient target for a request flood designed to exhaust CPU and database connections. Here the action may be missing or junk; the tell is volume from many sources with no legitimate pattern.
The honest rule: high admin-ajax.php traffic is usually your own Heartbeat and plugins, but "usually" is not "always". Read the action. A familiar action on the expected screens is your site. A strange action, hammered unauthenticated, is a lead.
What not to do (and why)
The instinct, especially after a security plugin flags the hits, is to block the file. Don't:
| Proposed action | Verdict | Why |
|---|---|---|
Deny admin-ajax.php in .htaccess or a firewall | Don't | This is the entry point for most WordPress AJAX. Blocking it breaks Heartbeat post-locking (the editing-collision warning), the classic editor, and every plugin's front-end and back-end AJAX. The block editor autosaves over the REST API so it survives, but plenty around it does not, and you get a site that silently fails to load dynamic content. |
| Disable admin-ajax.php entirely / rename it | Don't | Same breakage, harder to undo, and core and most plugins hardcode the path. |
Block all wp_ajax_nopriv_ requests site-wide | Careful | Blocking unauthenticated AJAX is a narrower hardening step, but plenty of legitimate front-end features (search-as-you-type, add-to-cart, public forms) run through nopriv. Block by specific action after you know what your site uses, not wholesale. |
Blocking the endpoint treats the symptom (a high number) and breaks the function. The number was never the problem.
The right fixes
Once you know which action is responsible, the fix is targeted:
- Throttle Heartbeat where it is safe. The Heartbeat Control plugin lets you set the interval, or disable Heartbeat, per location: keep it running in the post editor (where post-locking genuinely matters), and slow it down or turn it off on the dashboard and front end. Most performance caching plugins (WP Rocket and others) include the same control. Do not kill it everywhere by reflex; you lose the editing-collision warning (and on the classic editor, its autosave).
- Tame WooCommerce cart fragments. On modern WooCommerce (7.8+) the script is already scoped to pages with a cart widget. On older stores, disable cart fragments on pages that have no cart (the homepage, blog posts), or move to a server-side mini-cart. This removes a per-pageview POST from your busiest pages.
- Move legitimately heavy AJAX to the REST API. If you are writing the code, a custom REST API endpoint is the modern home for anything substantial, with proper authentication on write endpoints. admin-ajax.php bootstraps wp-admin on every call; a REST route does not have to. WooCommerce's own
wc-ajaxendpoint exists for exactly this reason. - Cache and rate-limit, don't block. If you are genuinely being flooded, a WAF or CDN rate-limit on admin-ajax.php (allow a sane request rate, challenge the rest) protects the endpoint without breaking it. That is different from denying it outright.
The throughline: identify the action, then act on that action. "admin-ajax.php is busy" is a starting point, not a diagnosis, and definitely not a reason to wall off the file your site depends on.
Sources
Authoritative references this article was fact-checked against.
- WordPress Developer - Heartbeat APIdeveloper.wordpress.org
- WordPress Developer - AJAX in WordPress (admin-ajax.php)developer.wordpress.org
- WordPress Core - Autosave and post locking (15s lock refresh vs 60s autosave)make.wordpress.org
- WooCommerce - Best practices for the cart fragments APIdeveloper.woocommerce.com
- WooCommerce - Custom AJAX endpoints in 2.4 (the wc-ajax endpoint)developer.woocommerce.com
- Heartbeat Control pluginwordpress.org
- WordPress Developer - Nonces (AJAX request verification)developer.wordpress.org





