admin-ajax.php returns 0 (or -1): the five causes
An AJAX request to wp-admin/admin-ajax.php comes back with nothing but 0. That 0 means WordPress found no handler for the action — or the handler forgot to stop. Here are the five reasons, in the order to check them, and what a -1 or a 400 means instead.
What the 0 actually means
admin-ajax.php reads one parameter, action, and fires the hook wp_ajax_{action} for a logged-in user or wp_ajax_nopriv_{action} for a visitor. Whatever your handler prints is the response. When the script finishes, it calls wp_die( '0' ). So a bare 0 means one of two things: no function was hooked to that action for this kind of user, or a function ran but did not stop the script itself, and WordPress appended its 0 to whatever it printed.
In current WordPress, a missing action, or one with no handler for this kind of user, returns 0 with HTTP status 400 — which is why the browser’s network tab may show “400 Bad Request” alongside the 0.
The five causes
- Only wp_ajax_ is registered, and the request comes from a logged-out visitor. Front-end features need both hooks: add_action( 'wp_ajax_my_action', ... ) and add_action( 'wp_ajax_nopriv_my_action', ... ). Testing while logged in hides this completely.
- The action parameter never arrived. admin-ajax.php reads $_REQUEST['action']. A fetch() call with a JSON body (Content-Type: application/json) sends no form fields at all, so PHP sees no action. Send FormData or application/x-www-form-urlencoded, or put action in the query string.
- The action name does not match. The hook is wp_ajax_ plus exactly the action value: action=save_cart needs wp_ajax_save_cart. A prefix added in one place and not the other, or a typo, is enough.
- The add_action call never runs during the AJAX request. If it sits inside a shortcode, a template file, or behind a check that is false in admin-ajax — ! is_admin(), for instance, which is false there because admin-ajax counts as admin — the hook is never registered. Register AJAX hooks at plugin load or on init, unconditionally.
- The handler does not end with wp_die(). It works — the data is saved — but the response is your output followed by 0, which breaks JSON.parse. End every handler with wp_send_json_success() / wp_send_json_error(), which send JSON and stop, or with wp_die().
A handler that works for everyone
add_action( 'wp_ajax_wk_save_note', 'wk_save_note' );
add_action( 'wp_ajax_nopriv_wk_save_note', 'wk_save_note' );
function wk_save_note() {
check_ajax_referer( 'wk_save_note', 'nonce' ); // dies with -1 on a bad nonce
$note = sanitize_text_field( wp_unslash( $_POST['note'] ?? '' ) );
// ... save it ...
wp_send_json_success( array( 'saved' => $note ) ); // sends JSON and stops
}const body = new FormData();
body.append('action', 'wk_save_note');
body.append('nonce', wkAjax.nonce);
body.append('note', 'Hello');
const res = await fetch(wkAjax.url, { method: 'POST', body });
const json = await res.json(); // { success: true, data: { saved: "Hello" } }wkAjax comes from wp_localize_script() or wp_add_inline_script(), with url set to admin_url( 'admin-ajax.php' ) and nonce to wp_create_nonce( 'wk_save_note' ).
If it returns -1 instead
-1 (usually with status 403) is a failed nonce check: check_ajax_referer() could not verify the nonce. The page was cached with an old nonce, the nonce was created for a different action name, or it was created for a logged-out visitor and checked after they logged in. Exclude the page from full-page cache, or fetch a fresh nonce before the request.
Seeing what the server got
- Browser dev tools → Network → the admin-ajax.php request → Payload: is action there, with the value you expect?
- Response tab: anything before the 0 is output from your handler that ran without stopping.
- Temporarily log the request at the top of the handler with error_log( print_r( $_REQUEST, true ) ); with WP_DEBUG_LOG on — if nothing is logged, the hook is not registered.
- Consider the REST API for new code: register_rest_route() gives you routes, methods, permission callbacks and JSON responses without these pitfalls.
