Wat admin-ajax.php is en wie het aanroept
admin-ajax.php is geen feature. Het is een dispatcher. Elk AJAX-verzoek dat een WordPress-plugin doet, landt op dezelfde URL (/wp-admin/admin-ajax.php) en het bestand leest de action-parameter uit de POST-body om te bepalen welke PHP-functie het verzoek moet afhandelen. Voor ingelogde gebruikers is de hook wp_ajax_{$action}, voor anonieme bezoekers wp_ajax_nopriv_{$action}.
De performancekosten zitten in de bootstrap. Elke aanroep naar admin-ajax.php laadt de volledige WordPress admin-omgeving: core, alle actieve plugins, alle hooks en de admin_init-action. Een simpele "geef een JSON-teller terug"-action boot tientallen megabytes PHP voordat er ook maar een regel plugincode draait. De REST API (onderdeel van core sinds WordPress 4.7, december 2016) slaat die admin-bootstrap over en is doorgaans 15–25% sneller voor vergelijkbare operaties, maar admin-ajax.php is niet deprecated. Honderden plugins gebruiken het nog, en de Heartbeat API draait er by design op.
Daardoor is "admin-ajax.php is je topverkeer" zoiets als "je startscherm is je meestgebruikte app." Het startscherm is niet het probleem. De app erachter wel.
admin-ajax.php vs wc-ajax vs de REST API
Niet elk AJAX-verzoek op een WordPress-site gaat daadwerkelijk door admin-ajax.php. WooCommerce introduceerde een eigen lichtgewicht endpoint in versie 2.4 (2015): /?wc-ajax=get_refreshed_fragments. Dit loopt via de wp_loaded-hook in plaats van de admin-ajax dispatcher, waardoor admin_init niet afvuurt. De URL is anders. De PHP-bootstrapkosten zijn vergelijkbaar maar niet identiek.
Als je access log of APM-tool veel verkeer toont naar /?wc-ajax= in plaats van /wp-admin/admin-ajax.php, dan is het probleem WooCommerce-specifiek en is de Heartbeat API niet betrokken.
De REST API (/wp-json/) is een derde, apart endpoint. Block editor autosave in WordPress 5.0+ gebruikt de REST API, niet admin-ajax.php. Steeds meer plugins migreren ernaartoe. Als de load onder /wp-json/ verschijnt, is dit artikel niet van toepassing.
| Endpoint | URL-patroon | Bootstrap | Typische aanroepers |
|---|---|---|---|
| admin-ajax.php | POST /wp-admin/admin-ajax.php |
Volledige admin-omgeving | Heartbeat, legacy plugins, formulierplugins, analytics-beacons |
| wc-ajax | GET /?wc-ajax=<action> |
wp_loaded (lichter) |
WooCommerce cart fragments, add-to-cart |
| REST API | GET/POST /wp-json/... |
REST-bootstrap (lichtst) | Block editor, moderne plugins, oEmbed |
Symptomen die wijzen op admin-ajax.php load
Het probleem ziet er anders uit, afhankelijk van waar je kijkt:
In hosting-dashboards. De tabel "top-URL's op aantal verzoeken" of "top-URL's op CPU-tijd" toont /wp-admin/admin-ajax.php bovenaan, soms met duizenden hits per uur. Dit is de meest voorkomende aanleiding voor de bekende e-mail van je hoster. Het probleem: access logs registreren alleen de URL, niet de POST-body. De action=-parameter die de veroorzaker identificeert, is onzichtbaar.
In APM-tools (New Relic, Datadog). De transactie WebTransaction/Action/admin-ajax.php staat bovenaan "meest tijdrovend" of "hoogste throughput". New Relic registreert dit als een enkele transactie, ongeacht welke action erin draait. De New Relic Reporting for WordPress plugin (van 10up) verrijkt transactienamen met WordPress-specifieke metadata, waardoor de trage action zichtbaar wordt in de segmentboom.
In browser developer tools. Op de voorkant laat het Network-tabblad in DevTools herhaalde admin-ajax.php POST-verzoeken zien die bij elke page load afvuren. Het Payload-tabblad onthult de action=-waarde. Zie je dit op de publieke site (niet in wp-admin), dan genereren anonieme bezoekers het ook.
In servermetrics. Het aantal PHP-FPM workers blijft hoog, ook buiten piekuren, en de pool raakt verzadigd. Omdat admin-ajax.php verzoeken uncacheable POST-requests zijn, claimen ze elk een PHP worker voor de volledige duur van het antwoord.
De veroorzaker identificeren
Het kernprobleem bij diagnostiek is dat standaard webserver access logs geen POST-bodies registreren. De URL /wp-admin/admin-ajax.php staat op elke regel, maar de action=-parameter die de verantwoordelijke plugin identificeert, is nergens te vinden. Je hebt een van deze methoden nodig:
Browser DevTools (snelst voor frontend-verkeer). Open Chrome of Firefox DevTools, schakel naar het Network-tabblad, filter op Fetch/XHR en herlaad de pagina of doorloop de gebruikersflow. Klik op een admin-ajax.php-regel en open het Payload-tabblad. Het action-veld is in plain text leesbaar.
Query Monitor (server-side, alleen voor admins). Installeer Query Monitor en reproduceer de flow. Het AJAX-paneel toont per verzoek de action-naam, het aantal queries, het geheugengebruik en de afgevuurde hooks. Output is standaard admin-only; gebruik QM's auth cookie-mechanisme om als niet-admin te testen.
WP Engine admin-ajax logging. Op WP Engine hosting voeg je dit toe aan wp-config.php:
define( 'WPE_MONITOR_ADMIN_AJAX', true );
Hiermee worden POST-bodies weggeschreven naar wp-content/__wpe_admin_ajax.log. Parseer het:
# Toon de 20 meest voorkomende actions
grep "action" wp-content/__wpe_admin_ajax.log \
| sort | uniq -c | sort -rn | head -20
APM-transactie tracing. In New Relic One: APM, dan Transactions, sorteer op "Most time consuming", filter op admin-ajax.php, drill in op een transactie-trace en zoek de trage hook in de segmentboom.
De plugin vinden bij een action-naam. Zodra je de action-naam hebt (zeg adrotate_impression), is de snelste aanpak even de action-naam online opzoeken: een zoekopdracht naar wp_ajax adrotate_impression levert bijna altijd de documentatie of het supportforum van de plugin op. Als de action-naam begint met een herkenbaar pluginprefix (woocommerce_, elementor_, wpforms_), is de eigenaar sowieso duidelijk.
Als je SSH-toegang hebt: zoek direct in je pluginmap:
grep -r "wp_ajax_nopriv_adrotate_impression\|wp_ajax_adrotate_impression" \
wp-content/plugins/
Dit geeft je de exacte plugin en het bestand dat de handler bezit.
Heartbeat-pieken oplossen
De Heartbeat API stuurt action=heartbeat naar admin-ajax.php met standaardintervallen van 15 seconden in de post-editor en 60 seconden op andere wp-admin schermen. Als je logs bevestigen dat heartbeat de topaction is, dan is throttlen de oplossing, niet uitschakelen.
Throttle het interval naar 60 seconden overal:
// In een mu-plugin of functions.php
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 60; // toegestaan bereik: 15–300 seconden
return $settings;
} );
Schakel Heartbeat uit op admin-schermen waar autosave niet uitmaakt (alles behalve de post-editor):
add_action( 'admin_init', function() {
$screen = get_current_screen();
if ( $screen && $screen->base !== 'post' ) {
wp_deregister_script( 'heartbeat' );
}
} );
Schakel Heartbeat niet helemaal uit. Dat breekt autosave en post locking. Met 50 redacteuren in de post-editor ga je van ruwweg 200 uncacheable PHP-verzoeken per minuut (bij 15-seconde intervallen) naar ongeveer 50 (bij 60-seconde intervallen). Als de Heartbeat API conceptartikel op jouw situatie van toepassing is, vind je daar het volledige verhaal.
Verificatie: na het deployen van het filter open je de post-editor, open je DevTools Network-tabblad en controleer je dat admin-ajax.php-verzoeken met action=heartbeat nu met 60-seconde intervallen verschijnen in plaats van 15.
Pluginveroorzaakte pieken oplossen
Als de topaction niet heartbeat is maar iets als woocommerce_get_cart_fragments, adrotate_impression, een page builder auto-draft action of een chatplugin session keepalive, hangt de oplossing af van de aanroeper.
WooCommerce cart fragments
De woocommerce_get_cart_fragments-action ververst het mini-cart widget bij elke page load. Op WooCommerce-versies voor 7.8 werd het wc-cart-fragments.js-script op elke pagina geladen, ongeacht of er een cart widget bestond. WooCommerce 7.8 (2023) veranderde dit naar conditioneel laden: het script vuurt alleen als een Cart Widget gerenderd is of de shortcode aanwezig is.
Fix: update WooCommerce naar 7.8+. Als een third-party theme of plugin wc-cart-fragments geforceerd enqueued, dequeue het conditioneel op niet-shoppagina's:
add_action( 'wp_enqueue_scripts', function() {
if ( ! is_woocommerce() && ! is_cart() && ! is_checkout() ) {
wp_dequeue_script( 'wc-cart-fragments' );
}
}, 99 );
Het WooCommerce Cart block (de block editor-versie, niet de legacy widget) gebruikt de fragments-API helemaal niet.
Analytics- en ad-rotatieplugins
Plugins die impressie-beacons of tracking-events via admin-ajax.php afvuren voor elke bezoeker zijn het duurste patroon. Een real-world WP Engine admin-ajax loganalyse vond een enkele AdRotate-installatie die 47.608 calls genereerde. De fix is meestal de plugin vervangen door een die client-side tracking of een REST API-endpoint gebruikt, of de feature helemaal uitschakelen.
Page builder auto-draft saves
Elementor, Avada en Visual Composer maken allemaal admin-ajax.php calls voor auto-draft saves en preview-refreshes. Die vuren alleen voor ingelogde editors, dus ze tellen pas mee op sites met veel gelijktijdige backendgebruikers. Throttling wordt doorgaans afgehandeld in het instellingenpaneel van de builder zelf.
Verificatie na elke pluginfix: monitor na de wijziging het admin-ajax log of je APM-tool gedurende 24 uur. Het aantal verzoeken van de specifieke action zou naar bijna nul moeten dalen, of naar het verwachte tarief.
Waarom admin-ajax.php cachen niet de oplossing is
POST-verzoeken naar admin-ajax.php zijn by design niet cachebaar. Standaard pagecaches (Nginx FastCGI cache, Varnish, WP Rocket, W3 Total Cache) sluiten ze terecht uit. Sommige CDN-configuraties kunnen geforceerd POST-responses cachen, maar dat voor admin-ajax.php doen veroorzaakt echte schade:
- Gecachte
action=heartbeat-responses betekenen dat alle gebruikers dezelfde autosave-status delen. Risico op dataverlies. - Gecachte cart fragment-responses betekenen dat alle bezoekers dezelfde winkelwagen zien. Een WooCommerce-shop die de winkelwagen van de ene bezoeker aan de andere serveert, is een afrekenramp.
- Gecachte nonce-gated actions serveren verlopen nonces. WordPress nonces verlopen na 12–24 uur, en een gecachte nonce-response breekt formulierinzendingen en beveiligingscontroles.
De oplossing is het aantal verzoeken verminderen, niet de responses cachen. Als een plugin-action geschikt is voor caching, is het juiste pad die te migreren naar een GET-gebaseerd REST API-endpoint dat veilig gecacht kan worden.
Wil je een compleet beeld van hoe WordPress cachelayers samenwerken en waar bypasses optreden, lees dan waarom je WordPress cache niet werkt.
Waarom admin-ajax.php blokkeren niet de oplossing is
admin-ajax.php leeft onder /wp-admin/, maar wordt legitiem aangeroepen vanaf de publieke voorkant. Plugins die live search, AJAX-contactformulieren, winkelwagenupdates en infinite scroll bieden, sturen allemaal verzoeken naar admin-ajax.php vanuit anonieme bezoekers. Het bestand blokkeren breekt:
- Elke plugin-AJAX feature op de voorkant
- WordPress automatische update-voortgangsrapportage
- Elke plugin die
wp_ajax_nopriv_gebruikt voor publieke features
Rate limiting per IP op webserver- of CDN-niveau (Nginx limit_req_zone, Cloudflare rate rules) is de juiste aanpak voor bots die admin-ajax.php bestoken. Blokkeer bots, niet het bestand.
Wanneer wp-cron via admin-ajax.php het echte probleem is
WordPress wordt geleverd met ALTERNATE_WP_CRON, een modus waarbij geplande taken afvuren via een redirect naar admin-ajax.php?action=wp_cron. Sommige hosters schakelen dit standaard in. Het resultaat is dat elke cron-run (plugin-updatechecks, backupschema's, e-mailwachtrijen, WooCommerce orderopruiming) via admin-ajax.php loopt en in je access log verschijnt onder dezelfde generieke URL.
Als je loganalyse een hoog aandeel verzoeken toont met action=wp_cron, is de oplossing WP-Cron vervangen door een echte systeemcron zodat geplande taken op een voorspelbaar interval draaien in plaats van mee te liften op bezoekersverkeer.
Wanneer escaleren
Als je de action hebt geidentificeerd, de bijbehorende fix hebt toegepast en de admin-ajax.php load na 24 uur niet merkbaar is gedaald, verzamel dan het volgende voordat je contact opneemt met je hoster of een developer:
- De top 10 actions uit je admin-ajax log of APM-transactieoverzicht
- PHP-versie en WordPress-versie (zichtbaar in wp-admin onder Gereedschap, dan Sitegezondheid, dan Info; of via
wp core version --extraals je WP-CLI hebt) - Lijst actieve plugins (zichtbaar in wp-admin onder Plugins; of via
wp plugin list --status=activemet WP-CLI) - Of het verkeer frontend is (anonieme bezoekers) of backend (ingelogde gebruikers), zichtbaar aan de
Referer-header in access logs - PHP-FPM poolconfiguratie:
pm,pm.max_children,pm.max_requestswaarden (vraag je hoster als je geen directe toegang hebt) - Of
ALTERNATE_WP_CRONis ingeschakeld inwp-config.php(controleer via de bestandsbeheerder van je hostingpanel of SFTP) - Stappen die je al hebt geprobeerd, met voor- en na-vergelijking van het aantal verzoeken
Herhaling voorkomen
- Audit nieuwe plugins voor activatie. Controleer of een plugin
wp_ajax_nopriv_-actions registreert die bij elke page load afvuren. Query Monitor toont dit in real time. - Geef de voorkeur aan plugins die de REST API gebruiken. De REST API slaat de admin-bootstrap over. Een plugin die
/wp-json/gebruikt voor zijn AJAX in plaats vanadmin-ajax.phpverbruikt minder CPU per verzoek. - Stel een echte systeemcron in. Het uitschakelen van
ALTERNATE_WP_CRONen WP-Cron draaien vanuit een crontab elimineert de hele klasse cron-via-admin-ajax verkeer. - Monitor de action-breakdown, niet alleen de URL. Een maandelijkse check van je top admin-ajax actions (via Query Monitor, APM of een 15 minuten durende WP Engine loganalyse) vangt nieuwe boosdoeners voordat ze een loadprobleem worden.