AJAX requests power everything in WordPress – from comment submissions to live search to cart updates. But when they break, the failure is silent: a spinner that never stops, a blank response, or a cryptic “0” in the browser console. This guide walks through every layer of the WordPress AJAX stack, shows you how to read the Network tab like a map, and gives you a repeatable debugging process that finds the problem fast.
How WordPress AJAX Works: The admin-ajax.php Flow
Before you can debug AJAX failures, you need a clear picture of what happens when a request fires. WordPress channels every AJAX call through a single entry point: wp-admin/admin-ajax.php. There is no routing framework, no controllers – just a file that checks a POST parameter called action and then fires the matching WordPress hook.
The sequence looks like this:
- Your JavaScript sends a POST request to
/wp-admin/admin-ajax.phpwith anactionparameter and any other data you need. - WordPress loads its core files (including all plugins and the active theme), but it does not render any page template.
- If the current user is logged in, WordPress fires the hook
wp_ajax_{action}. If they are logged out, it fireswp_ajax_nopriv_{action}. - Your registered callback handles the request, does its work, and calls
wp_send_json_success()orwp_send_json_error()to return a JSON response. - If no callback matches the action, admin-ajax.php outputs “0” and exits.
That last point explains one of the most common failure modes – the dreaded “0” response. It means your hook was never registered, or the action string in your JS does not match the action string in your PHP.
Registering wp_ajax_* and wp_ajax_nopriv_* Hooks
Every AJAX handler needs at least one of these two hooks. wp_ajax_{action} fires only for authenticated (logged-in) users. wp_ajax_nopriv_{action} fires for guests. If your feature needs to work for both, register both hooks pointing to the same callback.
The hook must be registered before admin-ajax.php processes the request. The safest place is a plain add_action() call at the top level of your plugin file, or inside an init callback. Registering inside a template function or inside a conditional that only runs on front-end page loads will cause the hook to be missed entirely.
Using the Browser DevTools Network Tab to Inspect AJAX Requests
The Network tab in Chrome DevTools (or Firefox DevTools) is your first stop every time an AJAX request misbehaves. Open it before you trigger the request – it does not capture traffic that happened before it was open.
Step-by-Step: Reading an AJAX Request
- Open DevTools with F12 (Windows/Linux) or Cmd+Option+I (Mac).
- Click the Network tab.
- In the filter bar, click Fetch/XHR to hide page assets and show only AJAX calls.
- Trigger the action on your page (button click, form submit, scroll event – whatever initiates the request).
- Look for a request to admin-ajax.php in the list. Click it.
Once you click the request row, you get four panels that matter:
| Panel | What to Look For |
|---|---|
| Headers | HTTP status code (200, 400, 403, 500). Request method should be POST. Check that Content-Type in the response is application/json. |
| Payload | The POST body your JS actually sent. Confirm action, nonce, and all expected fields are present with the right values. |
| Response | The raw text returned by the server. Should be {"success":true,"data":{...}} or similar JSON. |
| Preview | The parsed JSON tree. Easier to read than raw Response for complex objects. |
If the Response panel shows “0”, plain “false”, an HTML error page, or a PHP warning mixed into JSON, you have found your problem. Each of those outputs points to a different failure – covered in the next section.
Filtering and Preserving the Network Log
Check the Preserve log checkbox at the top of the Network panel. Without it, navigation or redirects wipe the log before you can read the response. Also check Disable cache while debugging to make sure you are always seeing fresh server responses.
If your AJAX request triggers a redirect, the Network tab will show the initial request and the redirect as two separate rows. The original response – the one with the error – is in the first row. Always check that one, not the redirect destination.
Common AJAX Failures and What They Mean
WordPress AJAX errors fall into a small set of repeatable patterns. Here is what each HTTP status code or response value actually tells you, and where to look next.
“0” Response (Plain Text)
This is the default output of admin-ajax.php when no hook matches the action. Causes:
- The action string in your JS does not match the string after
wp_ajax_in your PHP. - Your
add_action()call is inside a conditional that is not running (wrong hook, wrong page, wrong user role). - You registered
wp_ajax_{action}but the request comes from a logged-out user and you did not registerwp_ajax_nopriv_{action}. - Your plugin file is not active or the function file is not being included.
Fix: Add a temporary error_log( 'Handler reached' ); at the top of your callback. If it never appears in /wp-content/debug.log, the hook is not firing. Check your add_action() call location and action string spelling.
400 Bad Request
Your handler received the request but explicitly rejected it – most often because required POST data is missing or invalid. Check:
- Is the expected POST field actually in the Payload panel? A typo in the JS
formData.append()call will cause this. - Is your validation too strict? For example, using
intval()on a field that might be a string. - Is a required nonce field missing entirely? If
wp_localize_scriptran before the nonce was created, the nonce value could be empty.
403 Forbidden
Almost always a nonce failure or a capability check failure. The most common scenario: your nonce was created for one action string but verified against a different action string. The second most common: the user does not have the required capability (current_user_can() returns false) and your handler calls wp_send_json_error( ..., 403 ).
A 403 can also come from a security plugin or server firewall blocking the request before it reaches WordPress. If the Response body is HTML (a firewall block page) rather than JSON, that is the server – not your code.
500 Internal Server Error
A PHP fatal error or exception crashed the request before it could return a response. Enable WP_DEBUG and WP_DEBUG_LOG, trigger the request again, then read /wp-content/debug.log. The fatal error message will be there with the exact file and line number.
Common causes: calling a function that does not exist, passing the wrong type to a function, a database query failure when WPDB strict mode is active, or a memory limit exceeded by loading too much data in a single request.
Garbled or Mixed JSON (PHP Warning Prepended)
If WP_DEBUG_DISPLAY is on in a production environment, PHP warnings get printed before the JSON output. The response then looks like: Notice: Undefined variable $foo in handler.php on line 12 {"success":true...}. The JS parser chokes on this because it is no longer valid JSON.
Fix: set define( 'WP_DEBUG_DISPLAY', false ); and @ini_set( 'display_errors', 0 ); in wp-config.php. Errors go to the log file, not to the output stream.
Nonce Failures: Diagnosing and Fixing wp_create_nonce, wp_verify_nonce, and wp_nonce_field
Nonces are WordPress’s built-in request verification tokens. They prevent Cross-Site Request Forgery (CSRF) attacks by ensuring the request came from your own page. When they break, it looks like a 403 or a generic security failure. The debugging pattern is the same every time.
The Three Nonce Failure Scenarios
Scenario 1: Action string mismatch. wp_create_nonce( 'save_item' ) will only verify against check_ajax_referer( 'save_item'... ). If one side uses 'save_item' and the other uses 'save-item' or 'saveItem', verification will always fail. Define the action string as a constant in a shared file to avoid typos.
Scenario 2: Nonce created before the user was authenticated. If you create the nonce on a cached page (Varnish, Redis Object Cache, a full-page caching plugin), the nonce will be created for an anonymous user but then sent to a logged-in user (or vice versa). The session user does not match the nonce user, so verification fails. Solution: exclude AJAX nonce endpoints from caching, or regenerate nonces via a separate uncached endpoint.
Scenario 3: Nonce expiry. WordPress nonces expire after 24 hours by default (split into two 12-hour windows). If a user leaves a tab open overnight and then triggers an action, their nonce is invalid. For long-running sessions, refresh the nonce with a heartbeat or pass a freshly generated nonce in the response of each successful AJAX call so the client always has a current one.
Verifying Nonce Values in the Network Tab
Open the Payload panel in the Network tab and look for the nonce field in the POST body. Copy the value. Then in your PHP handler, temporarily log it with error_log( 'Received nonce: ' . $_POST['nonce'] ); alongside error_log( 'Expected for action: my_plugin_nonce' );. You can also log wp_verify_nonce( $_POST['nonce'], 'my_plugin_nonce' ) directly – it returns 1, 2, or false, making it easy to see exactly what failed.
Passing Data to JavaScript with wp_localize_script
wp_localize_script() is the official WordPress way to expose PHP-side values to JavaScript. It creates a global JavaScript object containing any data you pass, and it outputs it as an inline script tag immediately before the enqueued script loads. This ensures the data is always available before your script runs.
A few rules to follow:
- The handle passed to
wp_localize_script()must match the handle used inwp_enqueue_script()exactly. - Call
wp_localize_script()afterwp_enqueue_script(), not before. The script must already be registered. - Never hardcode
admin-url('admin-ajax.php')in a JavaScript file. The URL changes between environments (local, staging, production). Always pass it via localization. - Never hardcode nonce values in JS files – they could be cached and expire. Always generate them fresh on each page load via PHP.
In modern WordPress (5.8+), you can also use wp_add_inline_script() to pass JSON-encoded data before or after a script, which gives you more flexibility but requires you to handle the JSON encoding yourself.
wp_send_json_success and wp_send_json_error: Proper Usage
These two functions are the correct way to return data from an AJAX handler. They set the Content-Type header to application/json, JSON-encode your data, wrap it in the WordPress response envelope, and call wp_die() to cleanly terminate the request. Using echo or die() directly bypasses this and causes problems.
The response envelope always looks like this on the JavaScript side:
- Success:
{ "success": true, "data": { ... your data ... } } - Error:
{ "success": false, "data": { ... your error info ... } }
In your JavaScript, always check response.success before accessing response.data. A common mistake is checking the HTTP status code and assuming 200 means success – but WordPress always returns HTTP 200 from admin-ajax.php regardless of whether the business logic succeeded or failed. The real success indicator is response.data.success, not the HTTP code.
The HTTP Status Code Parameter
Both wp_send_json_success() and wp_send_json_error() accept an optional second parameter: an HTTP status code. Passing 400 or 403 makes error responses more semantically correct and lets your frontend catch errors at the HTTP level too (some fetch wrappers treat non-2xx responses as exceptions). Use it consistently for a cleaner API.
Using DOING_AJAX for Conditional Debug Logic
WordPress defines the DOING_AJAX constant to true at the very top of admin-ajax.php, before any plugins load. This means you can check it anywhere in your code to detect whether the current request is an AJAX call.
The most useful application during debugging: wrap verbose error_log() calls in an if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) block. This lets you log every incoming request detail without flooding the log with noise from regular page loads. Remove or comment out these blocks before going to production.
A subtler use: some WordPress functions behave differently in AJAX context. For example, wp_redirect() on an AJAX request does not make sense – the browser is not following redirects from XHR calls. Checking DOING_AJAX before calling redirect functions prevents this class of bugs.
Writing the Front-End AJAX Request Correctly
The front-end side is where many AJAX bugs start. A missing field, a wrong content type header, or a misread response structure can cause failures that look like server problems but are actually client-side mistakes. Here is the correct pattern using the native Fetch API:
Key points in this pattern:
- Always use FormData for POST requests – admin-ajax.php reads from
$_POST, which is only populated when the Content-Type isapplication/x-www-form-urlencodedormultipart/form-data. FormData handles this automatically. Sending raw JSON withapplication/jsonwill leave$_POSTempty. - action must match your hook suffix exactly – If your PHP hook is
wp_ajax_load_product, the action field must beload_product. - Always include the nonce – Even if your handler does not currently verify it, include the field. Adding verification later without the field means a broken change in production.
- Check json.success, not HTTP status – WordPress returns 200 even for errors. Read the response envelope.
- Catch network errors separately – The catch block on the fetch handles total failures (no server response, DNS failure, timeout). The
json.success === falsebranch handles application-level errors.
Step-by-Step AJAX Debugging Workflow
When an AJAX feature is broken and you are not sure where the problem is, follow this sequence. It moves from the network layer inward to the PHP layer, isolating the problem at each step.
Step 1: Confirm the Request is Actually Sending
Open the Network tab, filter by Fetch/XHR, trigger the action. Does a request to admin-ajax.php appear? If not, the JavaScript event listener is not attached, or the event that should trigger it is not firing. Check the Console tab for JS errors that may have crashed the script before it could send the request.
Step 2: Read the Payload
Click the request row, go to Payload. Confirm every expected field is present: action, nonce, and all your custom fields. Confirm the values look correct. A typo in field names or a missing field here explains most 400 errors and nonce failures.
Step 3: Read the Response
Go to the Response tab. What do you see?
- “0” – Hook not registered. Check action string and add_action location.
- “false” – Rare; usually means a direct
echo falsecall instead of using wp_send_json. - Valid JSON with success: false – Your handler ran but returned an error. Read the message field.
- HTML page (error page or login page) – Either a 500 crash or a redirect to the login page (user is not logged in and you only registered wp_ajax, not wp_ajax_nopriv).
- PHP notices prepended to JSON – WP_DEBUG_DISPLAY is on. Check debug.log for the full notice text.
Step 4: Enable WP_DEBUG_LOG and Re-trigger
In wp-config.php, add:
define( 'WP_DEBUG', true );define( 'WP_DEBUG_LOG', true );define( 'WP_DEBUG_DISPLAY', false );
Then trigger the AJAX call and check /wp-content/debug.log. Any PHP errors, notices, or your own error_log() calls appear here. This is the fastest way to catch fatal errors and undefined variable notices that silently corrupt the response.
Step 5: Add Instrumentation to Your Handler
Add targeted logging at key points in your callback:
- At the very top:
error_log( 'Handler: my_plugin_action reached' );– confirms the hook is firing. - After nonce check:
error_log( 'Nonce passed' );– confirms security is not blocking you. - After input validation:
error_log( 'item_id: ' . $item_id );– confirms you are receiving the right data. - After the main logic:
error_log( 'Result: ' . print_r( $result, true ) );– confirms the business logic ran correctly.
Work from the top down. If the first log line never appears, the hook is not firing. If it appears but the nonce log does not, the security check is failing. This binary isolation approach finds the exact failure point in minutes.
Step 6: Check Plugin and Theme Conflicts
If your handler code looks correct but the request still fails, another plugin might be intercepting it. Security plugins like Wordfence or iThemes Security sometimes block requests to admin-ajax.php that match their suspicious-request patterns. Temporarily deactivate security plugins and test again. If the request succeeds, add an exception rule in the security plugin for your action name.
Caching plugins can also serve a stale version of your localized script, which means the nonce value in the JS is days old and expired. Clear all caches and test again with the browser cache disabled. For a broader look at how to reduce external HTTP requests in WordPress, that guide covers caching strategy and script consolidation techniques that can help here too.
AJAX Error Response Reference Table
| Response | HTTP Code | Likely Cause | Where to Look |
|---|---|---|---|
| “0” | 200 | No hook matched the action | add_action() call, action string spelling |
| JSON success: false, message: “Nonce verification failed” | 200 or 403 | Wrong nonce action, expired nonce, cached page | wp_create_nonce action string, caching config |
| Empty body | 200 | Handler called return without wp_send_json | All exit paths in callback |
| HTML login page | 302 then 200 | User not logged in, only wp_ajax registered | Add wp_ajax_nopriv hook |
| HTML error page or stack trace | 500 | PHP fatal error in handler | debug.log |
| PHP notice + JSON | 200 | WP_DEBUG_DISPLAY is true | wp-config.php debug settings |
| 403 from firewall (HTML block page) | 403 | WAF or security plugin blocking the request | Security plugin rules, server firewall logs |
Advanced: Debugging AJAX in the WordPress REST API vs admin-ajax.php
Modern WordPress development sometimes uses the REST API (/wp-json/) instead of admin-ajax.php. If you are working on a newer plugin or theme, check whether the requests in your Network tab are going to /wp-admin/admin-ajax.php or /wp-json/. The debugging approach differs:
- REST API errors use HTTP status codes meaningfully (401, 403, 404, 500) – so the status code is a reliable indicator.
- REST API authentication uses cookies +
X-WP-Nonceheader (not a POST body nonce field). - REST API route not found returns 404 with a JSON error body, not “0”.
For REST API debugging, check whether the route is registered with rest_route_for_post or by visiting /wp-json/ in the browser to see all registered routes. For admin-ajax.php, everything flows through the action hook system described in this guide.
Checklist: Before You Ship Any AJAX Feature
Use this checklist for every new AJAX endpoint you build. It covers the failure points described in this guide and ensures your handler is production-ready.
- Both
wp_ajax_{action}andwp_ajax_nopriv_{action}registered if the feature is public-facing. - Nonce created with a specific action string and passed via
wp_localize_script(). - Nonce verified with
check_ajax_referer()as the first thing in the handler. - All input sanitized with
sanitize_text_field(),absint(),wp_kses_post(), or the appropriate sanitizer. - All exit paths call
wp_send_json_success()orwp_send_json_error()– no barereturn. - Front-end JS reads
json.success, not the HTTP status code. - WP_DEBUG_LOG enabled during testing. Debug output verified in debug.log.
- Response tested with Network tab: correct JSON structure, no prepended notices.
- Tested with logged-in user and logged-out user if applicable.
- Security plugin allowed list checked if running Wordfence or similar.
Frequently Asked Questions
Why does my AJAX handler return “0” even though I registered the hook?
The most likely reason: the hook is registered inside a function that only runs on front-end page loads (e.g., inside a wp_enqueue_scripts callback or a template function). The AJAX request hits admin-ajax.php, which does not run these functions. Move your add_action( 'wp_ajax_*' ) call to the top level of your plugin file or inside an init callback with no conditions.
Can I use the WordPress REST API instead of admin-ajax.php for everything?
Yes, and for new development it is often a better choice. The REST API gives you proper HTTP semantics, built-in authentication, schema validation, and a cleaner debugging experience. admin-ajax.php is older and simpler but still widely used in legacy codebases. Both approaches are valid – choose based on your project’s existing patterns and complexity requirements.
How do I test AJAX handlers without a browser?
Use curl or a tool like Postman to send POST requests directly to admin-ajax.php. You will need to supply a valid nonce, which you can generate from the command line with wp eval "echo wp_create_nonce( 'your_action' );" using WP-CLI. This is useful for isolating whether the problem is in the PHP handler or in the front-end JavaScript that calls it.
My AJAX works locally but fails on the staging server. What should I check?
First, check whether the staging server has a different caching configuration. Full-page caches can serve stale nonces. Second, check whether staging runs a different PHP version – functions available in PHP 8.1 may not exist in PHP 7.4. Third, check server-level security: some hosting environments (WP Engine, Kinsta, SiteGround) have WAF rules that block certain POST patterns. Check your hosting control panel for blocked requests or firewall logs.
Wrapping Up
WordPress AJAX debugging follows a consistent pattern: check the Network tab to see what is actually being sent and received, trace the problem to either the front-end payload, the PHP hook registration, the nonce verification, or the response format, and use WP_DEBUG_LOG plus targeted error_log() calls to isolate the exact line. The tools are all built into WordPress and your browser – no extra software required.
If you found this guide useful, the Debugging and Profiling series on TweaksWP covers related topics including query performance profiling, WP_DEBUG_LOG deep dives, and server-level performance instrumentation. Each article is built around real debugging sessions, not theoretical examples. If your AJAX handlers deal with user-submitted content, also read the guide on handling user-generated content moderation in WordPress without killing performance – many of the same hook patterns apply.
Ready to Squash Your AJAX Bug?
Bookmark this guide and the checklist at the end. The next time an AJAX request breaks, start with the Network tab, follow the six-step debugging workflow, and use the error reference table to identify the failure pattern in seconds. Most WordPress AJAX bugs resolve in under ten minutes once you know where to look.
admin-ajax.php AJAX JavaScript WordPress Debugging Tips wp_ajax
Last modified: April 6, 2026