WordPress returns 400 from admin-ajax.php when it cannot find a valid action callback or when application/security validation rejects the request. The endpoint itself can be healthy while one plugin feature fails.
Capture the exact request. Visiting admin-ajax.php directly without an action commonly returns 0 or 400 and is not a useful health test.
Record the request safely
In browser Network, trigger the feature once and inspect method, URL, status, request fields and response.
Preserve:
action name
logged-in/anonymous state
HTTP method/content type
nonce field name/presence
response text
timestamp and initiator script
Redact nonce values, cookies and personal form content.
Confirm the action is sent
WordPress dispatches based on $_REQUEST['action']. A missing/empty action, wrong field name or JSON body not parsed into request variables can fail before plugin code.
Inspect the final browser request, not only JavaScript source. Another script may modify/form-encode it.
Use FormData or URL encoding according to the callback design and set content type correctly.
Match logged-in and public hooks
Authenticated callbacks use wp_ajax_{action}; anonymous callbacks require wp_ajax_nopriv_{action}.
add_action( 'wp_ajax_example_action', 'handle_example_action' );
add_action( 'wp_ajax_nopriv_example_action', 'handle_public_example_action' );
Register public access only when intended, with validation and rate protection. Do not expose administrator operations through nopriv.
Validate nonces and capabilities
Expired/mismatched nonces or lost login cookies can make the callback reject. Refresh/login and compare.
check_ajax_referer( 'example_action', 'nonce' );
current_user_can( 'edit_posts' );
Nonce verification does not replace authorisation. Return a structured error and correct HTTP status without revealing security detail.
Inspect PHP logs and response
The callback can trigger a fatal or call wp_die() with a generic result. Match timestamp with PHP/WordPress logs.
Use wp_send_json_success() / wp_send_json_error() in site-owned code:
wp_send_json_error( array( 'message' => 'Request could not be completed.' ), 400 );
Never echo secrets or raw exceptions to visitors.
Check firewall and request limits
ModSecurity/Cloudflare can reject certain payloads, sizes or frequencies. Search events by URI and timestamp.
Create a narrow rule adjustment for the validated action, not a blanket bypass for all admin-ajax.php. Public AJAX is a frequent abuse target.
Large uploads belong to appropriate upload/REST mechanisms rather than oversized AJAX bodies.
Review cache and script versions
AJAX responses must not be publicly page-cached. A stale JavaScript bundle may send an old action/nonce field after plugin update.
Clear generated assets and use versioned script enqueueing. Confirm the anonymous browser receives the current file.
Avoid hard-coded ajaxurl on the public front end; localise/use the correct admin URL.
Version the browser/server contract
After an update, cached JavaScript may send fields expected by the old callback while PHP implements the new contract. Treat action name, field names, data types and response shape as a versioned interface.
Use wp_localize_script() or wp_add_inline_script() only for non-secret configuration and nonces tied to the current page. Enqueue with a file/version value so browsers receive the matching script. Never put API secrets in localised JavaScript.
Handle concurrency and retries
Autocomplete, autosave and filters can produce overlapping AJAX requests. A slower old response may overwrite a newer UI state, while double clicks can repeat server mutations.
Cancel obsolete browser requests where appropriate and make write callbacks idempotent with stable object/action references. Use database transactions/locks only as narrowly as required. Do not solve concurrency by globally serialising every admin-ajax.php request.
Verify expected users and errors
Test authenticated/anonymous paths as designed, valid/invalid nonce, validation failure and rate behaviour. Confirm only authorised data is returned.
Monitor action-specific 400/403 rates after deployment. Recurring care should keep AJAX contracts versioned; endpoint availability alone does not prove each registered action remains compatible.