WordPress Transient Caching Patterns: Advanced Use with Expiry Groups

WordPress transients are a simple key-value cache stored in the wp_options table (or in Redis/Memcached when a persistent object cache is active). Beyond the basic set_transient()/get_transient() calls, a disciplined pattern of expiry-group keys, serialised complex data, and hook-driven invalidation prevents stale content and database bloat from accumulating over time.

Problem: WordPress transients are used for caching but without a systematic approach — some expire, some do not, stale transients from uninstalled plugins persist in wp_options, and cache-busting during updates causes cache stampedes.

Solution: Design transients with versioned keys (my_plugin_v2_transient_{id}), set realistic expiries, and hook into save_post/edited_term to delete specific transients rather than calling delete_transient on every save. Prevent stampedes with a lock flag — check if a background process is already regenerating the transient before starting a second regeneration.


The code below shows a group-expiry pattern using a version key, how to cache paginated query results per page, automatic invalidation on post save, and a cleanup routine that removes orphaned transients from the options table.


 'news',
        'paged'          => $page,
        'posts_per_page' => 10,
        'no_found_rows'  => false,
    ] );

    $data = [
        'posts'      => $query->posts,
        'max_pages'  => $query->max_num_pages,
        'total'      => $query->found_posts,
    ];

    set_transient( $key, $data, HOUR_IN_SECONDS );
    return $data;
}

// ── 3. Clean up expired transients from the options table ─────────────────
// (Only needed when NOT using a persistent object cache)
function delete_expired_transients(): void {
    global $wpdb;
    $wpdb->query(
        $wpdb->prepare(
            "DELETE o, ot
             FROM {$wpdb->options} o
             INNER JOIN {$wpdb->options} ot ON ot.option_name = REPLACE(o.option_name,'_transient_timeout_','_transient_')
             WHERE o.option_name LIKE %s
               AND o.option_value < %d",
            $wpdb->esc_like( '_transient_timeout_' ) . '%',
            time()
        )
    );
}
add_action( 'wp_scheduled_delete', 'delete_expired_transients' );


NOTE: When a persistent object cache (Redis/Memcached) is active, transients are stored in the cache backend — not in wp_options — and the cleanup routine above is unnecessary; the cache backend handles expiry natively and far more efficiently.