Skip to content

Cache API: Cache non-existent users in WP_User::get_data_by() to prevent duplicate queries#11632

Open
MarcinDudekDev wants to merge 1 commit intoWordPress:trunkfrom
MarcinDudekDev:trac/46388
Open

Cache API: Cache non-existent users in WP_User::get_data_by() to prevent duplicate queries#11632
MarcinDudekDev wants to merge 1 commit intoWordPress:trunkfrom
MarcinDudekDev:trac/46388

Conversation

@MarcinDudekDev
Copy link
Copy Markdown

Fixes https://core.trac.wordpress.org/ticket/46388

Problem

When get_userdata() (or WP_User::get_data_by('id', $id)) is called for a non-existent user ID, it always executes a database query. If the same non-existent ID is looked up multiple times — for example, through repeated calls to get_userdata(), get_avatar(), or other functions that resolve user data — each call results in a duplicate SELECT * FROM wp_users WHERE ID = ... query.

Solution

Cache non-existent user IDs in a notusers array within the users object cache group, following the established notoptions pattern from the options API (get_option()).

WP_User::get_data_by() (when $field === 'id'):

  1. Before querying the database, check the notusers cache. Return false immediately if the ID is cached as non-existent.
  2. After a database miss, add the ID to the notusers cache.

update_user_caches():

  • When a user is created or updated, remove their ID from the notusers cache to ensure stale negative-cache entries are invalidated. This addresses the concern raised in comment:1 about cache invalidation on user creation.

The users cache group is already registered as a global group in multisite (wp_cache_add_global_groups()), so the notusers key inherits that scope correctly.

Tests

Four new tests in tests/phpunit/tests/user/getDataBy.php:

  • Verifies second call for a non-existent user triggers 0 DB queries.
  • Verifies non-existent user ID is added to notusers cache after first miss.
  • Verifies notusers cache is invalidated when update_user_caches() is called.
  • Verifies existing user IDs are never added to notusers.

AI assistance: Yes
Tool(s): Claude Code (Anthropic)
Model(s): Claude Sonnet 4.6
Used for: Implementation and test authorship; reviewed and verified by contributor.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props myththrazz, apermo.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

…ent duplicate queries.

When get_userdata() or WP_User::get_data_by('id', $id) is called for a non-existent
user ID, the result is now cached as a per-ID key ('notuser_$id') in the 'users' cache
group. Subsequent calls return false immediately without hitting the database. The cache
entry expires after one day as a safety net for persistent object cache backends.

The cache is invalidated by clean_user_cache(), which is the canonical invalidation
path called by both wp_insert_user() and wp_update_user(). When called with a plain
integer (as wp_insert_user() does), the notuser key is cleared *before* constructing
a new WP_User internally, preventing a self-referential cache miss.

Using per-ID keys (rather than a shared array) avoids unbounded memory growth under
persistent object cache backends and eliminates read-modify-write race conditions.

Fixes #46388.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
);
if ( ! $user ) {
if ( 'id' === $field ) {
wp_cache_set( 'notuser_' . $user_id, true, 'users', DAY_IN_SECONDS );
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too sure about 1 day caching duration. Plus this cache will only be cached if the user has some persistent object cache.

@MarcinDudekDev
Copy link
Copy Markdown
Author

Thanks for the review @apermo!

On the TTL: The DAY_IN_SECONDS is intentional rather than arbitrary. On sites running Redis with volatile-lru eviction policy (a common configuration), keys without a TTL (0) become non-evictable under memory pressure — they never expire and compete with legitimate user data for cache slots. Adding a TTL makes the notuser_ entries properly evictable, which is actually the safer behaviour for persistent cache deployments. That said, I agree 24 hours is on the longer side for a safety net. I'm happy to reduce it to HOUR_IN_SECONDS if you feel that's more appropriate — the clean_user_cache() invalidation handles the correctness case; the TTL is only a fallback for edge cases like direct DB writes bypassing the WP API.

On persistent object cache: wp_cache_set() always runs — WordPress's default in-memory WP_Object_Cache is loaded on every request even without Redis or Memcached. Without a persistent drop-in, the notuser_ entry lives for the duration of the current request (TTL is silently ignored by the default cache), preventing duplicate DB queries within the same page load — for example, rendering multiple comment avatars for deleted users. The cross-request benefit does require a persistent backend, but that's true of all WordPress object caching, not specific to this patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants