WordPress Hook Priority: The Subtle Bug That Wasted My Afternoon

Last Tuesday I spent three hours tracking down why a custom post type’s REST API response was missing fields that I could clearly see being added in my plugin code. The function was hooked in. The code was correct. It just wasn’t running. The culprit? Hook priority.
How WordPress Hook Priority Works
When you call add_action() or add_filter(), the third parameter is priority — a number that determines execution order. Lower numbers run first. The default is 10.
add_filter('rest_prepare_post', 'add_custom_fields', 10, 3);
add_filter('rest_prepare_post', 'remove_unwanted_fields', 20, 3);
add_filter('rest_prepare_post', 'format_response', 30, 3);
In this example, add_custom_fields runs first (priority 10), then remove_unwanted_fields (20), then format_response (30). Each function receives the output of the previous one.
The Bug
My plugin was adding extra fields to the REST API response for a custom post type:
add_filter('rest_prepare_adz_project', 'add_project_meta', 10, 3);
function add_project_meta($response, $post, $request) {
$response->data['project_url'] = get_post_meta($post->ID, 'project_url', true);
$response->data['tech_stack'] = get_post_meta($post->ID, 'tech_stack', true);
return $response;
}
The fields weren’t appearing. I added error logging — the function wasn’t being called at all. I checked the hook name, triple-checked spelling, deactivated every other plugin. Nothing.
Then I found this in another file within the same plugin, added months earlier:
add_filter('rest_prepare_adz_project', 'sanitize_project_response', 10, 3);
function sanitize_project_response($response, $post, $request) {
$allowed_fields = ['id', 'title', 'content', 'slug', 'featured_media'];
$response->data = array_intersect_key(
$response->data,
array_flip($allowed_fields)
);
return $response;
}
Both functions had priority 10. WordPress executes same-priority hooks in the order they were registered. The sanitize function was registered first (loaded from an earlier file alphabetically), so it ran first and stripped the response down to only whitelisted fields. My add_project_meta function ran second and added the custom fields — but it was working on a response that had already been sanitized. The fields were added after sanitization, so everything seemed fine in the function itself.
Except it wasn’t. The sanitization function was also registered as a rest_post_dispatch filter with a broader scope that ran after all the rest_prepare_* filters, wiping my additions again. A double-registration I’d completely forgotten about.
The Fix
I changed my function to run at priority 99, well after sanitization:
add_filter('rest_prepare_adz_project', 'add_project_meta', 99, 3);
And removed the redundant rest_post_dispatch hook that was running the sanitizer a second time.
Debugging Hooks: The Tools
WordPress doesn’t make it easy to see what’s hooked where. Here’s how I debug hook issues now:
// Dump all callbacks on a specific hook
function debug_hook_callbacks($hook_name) {
global $wp_filter;
if (!isset($wp_filter[$hook_name])) {
error_log("No callbacks on hook: {$hook_name}");
return;
}
foreach ($wp_filter[$hook_name]->callbacks as $priority => $callbacks) {
foreach ($callbacks as $id => $callback) {
$func = $callback['function'];
if (is_array($func)) {
$name = (is_object($func[0]) ? get_class($func[0]) : $func[0]) . '::' . $func[1];
} elseif (is_object($func)) {
$name = 'Closure';
} else {
$name = $func;
}
error_log("Hook {$hook_name} | Priority {$priority} | {$name}");
}
}
}
Call debug_hook_callbacks('rest_prepare_adz_project') and check your error log. You’ll see every registered callback and its priority.
Rules I Follow Now
- Never use priority 10 for anything important. It’s the default, which means it collides with everything.
- Use descriptive priorities: 5 for early setup, 20 for normal processing, 50+ for sanitization/cleanup, 99 for “must run last.”
- Search your entire codebase for the hook name before adding a new callback. A quick
grep -r 'rest_prepare_adz_project'would have saved me three hours. - Document non-obvious priorities with a comment explaining why it’s not the default.
Hook priority is one of those WordPress concepts that seems trivial until it bites you. When your code is correct but not working, check what else is hooked to the same action.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


