The Caching Model (SWR)

TableCrafter renders tables instantly by serving cached output and refreshing the source in the background. This page explains the two transient layers, the one-hour cache lifetime, the five-minute stale window, and how to purge the cache.

Stale-While-Revalidate WordPress Transients Background Cron Cache Warming WP-CLI Purge

Why SWR

Most of TableCrafter's data lives on slow third parties: remote JSON APIs, Google Sheets, CSV files, and Airtable. Hitting those on every page view would make your tables wait on someone else's server. The stale-while-revalidate (SWR) strategy avoids that: the first visitor pays the fetch cost, every visitor after that gets the cached HTML immediately, and the cache quietly updates itself in the background once it gets old enough.

The net effect is an instant time-to-first-byte for the [tablecrafter] shortcode while the underlying data still tracks the source within a few minutes.

The Two Cache Layers

TableCrafter keeps two separate caches, both stored as standard WordPress transients (so an object cache like Redis or Memcached is used automatically when present, otherwise the wp_options table).

LayerTransient prefixWhat it holdsLifetime
HTML cachetc_html_Fully rendered, server-side table markup plus the raw data and a timestamp1 hour
Data cachetc_cache_Parsed JSON/CSV data for a single source URL1 hour (5 min for Airtable)

The HTML cache is what gives you the instant render. The data cache sits one level down and is reused by the AJAX proxy, the background refresher, and the cron warmer so a single fetch can feed several callers.

ℹ️

Two other prefixes share the same plumbing: tc_export_ for generated XLSX/PDF export files and tc_rate_ for AJAX rate-limit counters. They are purged alongside the table caches.

Cache Lifetime & the Stale Window

Three constants in includes/class-tc-cache.php govern the timing:

ConstantValueMeaning
HTML_CACHE_TTL1 hourHow long rendered HTML survives before WordPress expires the transient entirely.
DATA_CACHE_TTL1 hourHow long fetched source data survives.
STALE_THRESHOLD300 (5 minutes)Age after which a cached entry is considered stale and a background revalidation is triggered, even though it is still served.

The distinction matters: an entry is stale after 5 minutes but expired only after 60 minutes. Between those two marks, TableCrafter keeps serving the stale copy while it refreshes — that is the "stale-while-revalidate" behaviour. Airtable sources cap their data cache at 5 minutes instead of an hour, because Airtable's API enforces tighter rate limits.

How a Request Flows

When the [tablecrafter] shortcode renders, the engine builds a cache key and checks the HTML transient first.

  1. Cache miss (cold). The source is fetched synchronously, the table is rendered server-side, and the result is stored under the tc_html_ key for one hour. This is the only request that waits on the source.
  2. Cache hit, fresh (< 5 min). The stored HTML is returned as-is. No fetch, no cron.
  3. Cache hit, stale (> 5 min, < 1 hour). The stale HTML is still returned immediately, but a one-off background job is scheduled to rebuild it:
    if (time() - $timestamp > (5 * MINUTE_IN_SECONDS)) {
        if (!wp_next_scheduled('tc_refresh_single_source', [$atts])) {
            wp_schedule_single_event(time(), 'tc_refresh_single_source', [$atts]);
        }
    }
    The visitor never sees the fetch — the next visitor gets the refreshed copy.

The HTML cache key is a hash of the source plus the attributes that change the rendered output, so two shortcodes with different settings never collide:

// Distinct cache keys for distinct configurations
[tablecrafter source="https://api.example.com/products.json" include="name,price" sort="price:desc"]
[tablecrafter source="https://api.example.com/products.json" search="true" per_page="25"]

The key folds in source, include, exclude, search, filters, export, per_page, sort, and the plugin version. Note the version is part of every key — upgrading TableCrafter naturally invalidates every cached entry, so you never serve markup from an old release.

Background Refresh & Cache Warming

Two scheduled actions keep caches warm so visitors rarely hit a cold miss:

HookScheduleWhat it does
tc_refresh_single_sourceOne-off, on demandRebuilds one stale table when a visitor triggers the SWR path.
tc_refresher_cronHourlyRe-fetches every tracked source URL and refreshes its tc_cache_ data entry.

Source URLs are remembered automatically. Whenever the AJAX proxy fetches a URL, TableCrafter records it in the tc_tracked_urls option (capped at the 50 most recent). The hourly cron walks that list and warms each one, so popular tables stay fresh without anyone waiting.

⚠️

Both background jobs ride on WP-Cron, which only fires when your site receives traffic. On low-traffic sites a stale entry may persist until the next visit. If you depend on tight refresh windows, wire wp-cron.php to a real system cron, or run a manual warm (below).

Manually Purging the Cache

The fastest way to force everything to rebuild is the bundled WP-CLI command:

# Delete every TableCrafter transient (tc_html_, tc_cache_, tc_export_, tc_rate_)
wp tablecrafter clear-cache

# Re-fetch and re-cache all tracked source URLs right now
wp tablecrafter warm-cache

clear-cache reports how many transients it removed. A clean purge-then-rebuild is simply the two commands run back to back. If you do not have shell access, the same effect is reached by:

For programmatic control, the TC_Cache singleton exposes the underlying methods used by the CLI: clear_all() (purges every TableCrafter transient and returns the count), clear_source( $url ) (drops the data cache for one source), and warm_cache() (re-fetches all tracked URLs).

// Purge from a plugin, snippet, or a deploy hook
$cache = TC_Cache::get_instance();
$cache->clear_source('https://api.example.com/products.json'); // one source
$removed = $cache->clear_all(); // everything

Server Cache vs. Live Auto-Refresh

Do not confuse the server-side SWR cache with the optional client-side auto-refresh. They are independent. The SWR cache decides how fresh the initial server render is; auto-refresh polls the source from the browser after the page loads, on its own interval.

AttributeDefaultEffect
auto_refreshfalseEnables in-browser polling of the data URL.Optional
refresh_interval300000Poll interval in milliseconds (default 5 minutes).Optional
refresh_indicatortrueShows the live .tc-refresh-indicator control with pause and manual-refresh buttons.Optional
refresh_countdownfalseDisplays a .tc-countdown to the next poll.Optional
refresh_last_updatedtrueShows a relative "Updated …" timestamp in .tc-last-updated.Optional
[tablecrafter source="https://api.example.com/live-scores.json"
             auto_refresh="true" refresh_interval="60000" refresh_countdown="true"]

The indicator's manual button calls refreshNow(), which runs performRefresh() immediately and resets the interval. Auto-refresh smart-pauses while a user is interacting with the table and when the browser tab is hidden, then resumes automatically. Because the browser fetch goes through the same AJAX proxy, it still benefits from the data cache.

💡

For data that changes by the minute (scores, stock, queues), pair a short refresh_interval with auto_refresh="true" so visitors who keep the page open see updates without reloading. For mostly-static reference tables, leave auto-refresh off and rely on the one-hour SWR cache alone.

Inspecting & Tuning

Next, see data-sources.html for how each source type is fetched and parsed before it is cached, and shortcode-reference.html for the full list of attributes that influence the cache key.