Course 8’s performance lesson cached an ability’s execute_callback, the query behind
a “list orders” call. That’s caching a WordPress read. This lesson caches something
different: the AI-generated output itself, the actual text or JSON a model returned for
a given prompt. The two look similar on the surface, both are wp_cache_set() calls
with a key and a TTL, but an AI response has a cost dimension a database query doesn’t.
Every uncached call to generate_text() is a billed request to a model provider, so a
cache hit here isn’t just a faster response, it’s a call you didn’t pay for. The harder
problem isn’t writing the cache, it’s knowing which AI responses are actually safe to
reuse and which ones defeat the entire point of the feature the moment they go stale.
Comfortable calling wp_ai_client_prompt()->generate_text() (Course 3), and the basic
idea of set_transient() / get_transient() and wp_cache_get() / wp_cache_set()
from Course 8’s performance lesson. This lesson assumes you know those functions exist
and goes straight into using them for AI output specifically.
Step 1: Key the cache on the actual input, not a proxy for it
The most common mistake in caching an AI response is keying it on something adjacent to the prompt, a post ID, a feature name, instead of the prompt itself. If two different inputs produce the same key, the second request silently gets the first request’s output. Build the key from a hash of everything that affects the result: the prompt text, the system instruction, and any parameter (temperature, model preference) that changes what comes back.
// File: my-plugin/class-ai-cache.php
function my_plugin_ai_cache_key( string $prompt, string $system_instruction, string $model_hint = '' ): string {
$fingerprint = wp_json_encode( array(
'prompt' => $prompt,
'system' => $system_instruction,
'model_hint' => $model_hint,
) );
return 'my_plugin_ai_' . md5( $fingerprint );
}
Keying on post_123 instead of a hash of the actual prompt text feels simpler, but it
silently breaks the moment the post is edited: the cache key stays the same while the
content behind it changes, and the AI response you serve no longer matches what you
generated it from. Hash the real input. If you also want a post-scoped key for easy
invalidation (covered next lesson), store it as a second, separate mapping rather than
replacing the content hash with it.
Step 2: Choose transients or the object cache, deliberately
Both work for this. The difference is what they’re good at:
If you don’t know whether the target site has a persistent object cache configured, transients are the safer default, they behave consistently either way. If you control the hosting environment and know a persistent backend is present, the object cache gives you cache groups, which matters a great deal for the invalidation strategy in the next lesson.
// File: my-plugin/class-ai-cache.php
function my_plugin_get_cached_ai_response( string $key ) {
$cached = get_transient( $key );
return false === $cached ? null : $cached;
}
function my_plugin_set_cached_ai_response( string $key, string $value, int $ttl_seconds ): void {
set_transient( $key, $value, $ttl_seconds );
}
Step 3: Wrap the call, don’t scatter caching logic across every feature
A caching wrapper that every AI feature calls through means the caching decision lives
in one place, not copy-pasted into every function that calls generate_text():
// File: my-plugin/class-ai-cache.php
function my_plugin_generate_text_cached( string $prompt, string $system_instruction, int $ttl_seconds ) {
$key = my_plugin_ai_cache_key( $prompt, $system_instruction );
$cached = my_plugin_get_cached_ai_response( $key );
if ( null !== $cached ) {
return $cached;
}
$result = wp_ai_client_prompt( $prompt )
->using_system_instruction( $system_instruction )
->generate_text();
if ( is_wp_error( $result ) ) {
return $result;
}
my_plugin_set_cached_ai_response( $key, $result, $ttl_seconds );
return $result;
}
Notice errors are never cached, only a successful string result is stored. Caching a
WP_Error would mean a transient provider outage gets replayed as a failure for every
request until the TTL expires, which is worse than the outage itself.
Step 4: Decide whether the feature can tolerate a cache at all
This is the actual judgment call, and it matters more than the mechanics above. Caching an AI response is safe when the task is deterministic-in-intent and the underlying input doesn’t change on its own:
| Task | Cache? | Why |
|---|---|---|
| Summarizing a published blog post’s body | Yes | The source text is fixed once published; the same input should produce a stable summary, and regenerating it on every page view wastes a call on unchanged content. |
| Generating alt text for an uploaded image | Yes | The image doesn’t change after upload; one generation, reused indefinitely (or until the image itself is replaced), is correct. |
| A conversational support-chat reply | No | The whole point is a response to what the user just said, in context. Caching by prompt hash would occasionally serve one user’s reply to another user who happened to type the same words. |
| ”What’s trending right now” style copy | No | Explicitly time-sensitive; a cached answer is a stale, wrong answer by design, not just an old one. |
| An AI-drafted reply a human reviews before sending | Depends | Safe to cache the draft while the human hasn’t acted on it yet; once sent, the cached value has served its purpose and should not resurface for a different, later situation. |
The dividing line isn’t “is this text generation,” it’s “does this specific call need to reflect something that changes independently of the input I’m hashing.” A summarization task’s only real input is the source text. A conversational reply’s real input includes the moment it was asked in, something no prompt hash can capture.
Caching every AI feature by default because caching is cheap and calls are expensive is a real temptation once cost becomes visible (next module). Resist it for anything a user experiences as a live response. A support widget that occasionally repeats a stranger’s cached answer erodes trust faster than a slightly slower response would.
Test it
Call the wrapper twice with an identical prompt and confirm the model is only actually invoked once:
wp-env run cli wp eval '
$prompt = "Summarize in one sentence: This plugin adds AI-powered alt text generation.";
$system = "You are a concise technical summarizer.";
$first = my_plugin_generate_text_cached( $prompt, $system, 300 );
echo "First call: " . $first . "\n";
$second = my_plugin_generate_text_cached( $prompt, $system, 300 );
echo "Second call (should be instant, identical): " . $second . "\n";
'
Both calls should return identical text, and the second should return noticeably faster since it never reaches the model provider.
Recap
Caching an AI response is not the same problem as caching a database query: the key must be a hash of the actual prompt and its parameters, never a proxy like a post ID, and errors should never be cached. Transients are the safer default; the object cache is worth using deliberately when a persistent backend (Redis or Memcached) is actually configured, since it enables cache groups for the invalidation work in the next lesson. Above all, caching is a decision about the task’s shape, not a default: deterministic summarization of unchanging content is the clean case, anything meant to feel live or conversational is not.
Resources & further reading
- Transients API, developer.wordpress.org
- WP_Object_Cache class reference, developer.wordpress.org
- PHP AI Client SDK repository, GitHub