Data Storage#

How Docket Cache writes cached data to disk as plain PHP files, and how it reads it back.

Docket Cache is a persistent object cache for WordPress. Every cached item is written to disk as a small PHP file that returns the value. Reading an item is a plain include, so with OPcache enabled the value is loaded from shared memory without any parsing or unserializing.

To get that benefit, values are stored as native PHP (arrays, stdClass, scalars) wherever possible. Serialized strings are only used where storing native PHP is not safe or not possible.

This page describes Docket Cache 26.04.11 and later.

Data enters the cache in three ways:

  1. wp_cache_set() and related functions, called by WordPress core and plugins at runtime.
  2. Transients, which WordPress routes through the object cache (the transient and site-transient groups).
  3. Database import, which copies existing transients and autoloaded options from the database into the cache when the drop-in is installed or reinstalled.

The cache file#

Location and name#

wp-content/cache/docket-cache/<group hash>-<key hash>.php
  • Each hash is the first 12 characters of the md5() of the group and of the key.
  • With DOCKET_CACHE_CHUNKCACHEDIR enabled, files are spread over two levels of subfolders built from the hashes.
  • On multisite, keys are prefixed with the site ID, except for global groups such as users, site-options and site-transient. See DOCKET_CACHE_GLOBAL_GROUPS.
  • The folder is set by DOCKET_CACHE_PATH.

Contents#

<?php
return array (
  'timestamp' => 1791986760,
  'site_id' => 1,
  'group' => 'site-transient',
  'key' => 'update_plugins',
  'type' => 'object',
  'sv' => 3,
  'timeout' => 1792029960,
  'data' => (object) array( 'last_checked' => 1791986760, 'response' => array( ... ) ),
);
/*@DOCKET_CACHE_EOF*/
Field Meaning
timestamp When the file was written. Used with DOCKET_CACHE_MAXTTL to limit the file's age.
network_id Multisite only.
site_id ID of the site that wrote the item.
group, key The stored group and key. Checked on read, so a hash collision returns a miss, not another item's value.
type How data is stored. Decides what happens on read, see Stored types.
sv On-disk format version. Files with an older version are deleted on read. Currently 3.
timeout Absolute expiry timestamp, or 0 for no expiry. The file's age is still limited by DOCKET_CACHE_MAXTTL.
data The value.

If the value contains instances of classes other than stdClass, the file starts with a check that returns false (a cache miss) when the class is not loaded yet. Including the file never triggers a "class not found" error.

Writing to the cache#

wp_cache_set(), wp_cache_add(), wp_cache_replace() and wp_cache_set_multiple() all follow the same steps.

  1. Memory first. The value is kept in memory for the rest of the request. Objects are cloned, so changing an object after wp_cache_set() does not change the cached copy.
  2. Non-persistent items stay in memory. Groups in DOCKET_CACHE_IGNORED_GROUPS (default themes, counts, plugins), keys in DOCKET_CACHE_IGNORED_KEYS and pairs in DOCKET_CACHE_IGNORED_GROUPKEY are never written to disk.
  3. Expiry. A requested expiry is used as given. With no expiry, a default is picked by group: for example 1 day for transients, 14 days for options and core object groups, 28 days for the update_* site transients, and DOCKET_CACHE_MAXTTL (default 4 days) for everything else.
  4. Empty values. '' and [] are not written for the transient groups, and any existing file is removed.
  5. Size limit. Values larger than DOCKET_CACHE_MAXSIZE (default 3MB, measured serialized) are not written.
  6. Type. The value is stored according to the table below, and exported as PHP code with var_export(), or with the bundled Symfony VarExporter when it contains class instances.

What is stored#

Value passed in Stored type data in the file
int, float, bool integer, double, boolean native
null string ''
string string the string
string that looks serialized (e.g. a:1:{...}) string_serialize the exact string
array, stdClass array, object native
object of another class object VarExporter code
array containing class instances, in a transient group or with a Requests object array_serialize serialize() of the array

Docket Cache never unserializes what it is given. A plugin that caches the string 'a:1:{i:0;i:1;}' gets the same string back, not an array.

Reading from the cache#

wp_cache_get() checks memory first. On a memory miss it reads the cache file:

  1. Format version. A file with an sv lower than the current version is deleted and treated as a miss. This is how an update replaces old cache files without a manual flush.
  2. Key and group. If the stored key or group does not match the request, it is a miss.
  3. Expiry. A file past its timeout is deleted and treated as a miss. A file older than DOCKET_CACHE_MAXTTL is deleted, but its value is still returned for this request.
  4. Empty transients. A transient stored as '' is returned as false.
  5. Unpack by type, see the next section.
  6. The value is kept in memory for the rest of the request.

With DOCKET_CACHE_PRECACHE enabled, the keys read on a URL are recorded and loaded together at the start of the next request to the same URL. The values are the same as with a normal read.

Stored types#

type Written by On read
integer, double, boolean, string, array, object cache writes, database import returned as is (native PHP)
string_serialize cache writes (a caller's serialized-looking string), database import (a double-serialized value) returned as is
array_serialize cache writes (Docket Cache serialized the array itself) unserialized
db_serialize database import only (a raw serialized value it did not convert) unserialized, as get_option() does

Only types that Docket Cache itself produced from trusted data are ever unserialized:

  • array_serialize is created by Docket Cache from a native array it was given.
  • db_serialize is created only from rows in the WordPress database, which WordPress itself unserializes in get_option().

A caller's string is always string_serialize and is never unserialized.

Why serialized-looking strings are not unserialized#

Up to 26.04.09, strings that looked serialized were unserialized on save or on read. That changed the caller's data (a cached string came back as an array or object) and allowed PHP object injection: any string that reached the cache could create objects when it was read. Since 26.04.10, a caller's string is returned exactly as stored.

WordPress itself relies on this. get_option() caches the raw database string in the options group and calls maybe_unserialize() after wp_cache_get(), so returning the string unchanged gives the same result as WordPress without an object cache.

Incrementing and decrementing#

wp_cache_incr() and wp_cache_decr() write the new number back to the existing file. The stored type is reset to match the new value, so a number is never stored under a serialized type.

Transients#

By default, transients live only in the object cache, as with any persistent object cache:

  • set_transient() and set_site_transient() write a cache file in the transient or site-transient group.
  • The expiry comes from the transient's own expiration, or from the defaults above when none is given.
  • '' and [] are not stored, so they read back as false.

Transients in the database#

When DOCKET_CACHE_TRANSIENTDB is enabled, transients are stored in the WordPress database (wp_options, or wp_sitemeta on multisite) instead of in cache files, through the normal add_option() and update_option() calls, and read back with get_option().

Transients listed in DOCKET_CACHE_IGNORED_TRANSIENTDB (default doing_cron, update_plugins, update_themes, update_core) still use cache files. The database import below does not run in this mode.

Database import on drop-in install#

When it runs#

When the drop-in (wp-content/object-cache.php) is installed or replaced, for example after a plugin update changes its version, Docket Cache first runs a short delay phase:

  1. The object cache is off, and transient writes to the database are suppressed.
  2. At the end of the request, data is copied from the database into cache files.
  3. Transient rows that are expired or have no expiry are then deleted from the database. Rows with a future expiry are kept.
  4. The object cache is switched on for the next request.

Without this step, transients that WordPress or plugins stored in the database before the object cache was active would be lost, because WordPress no longer reads transients from the database once an object cache is running.

What it copies#

  • Transients: _transient_* and _site_transient_* rows from wp_options (and wp_sitemeta on multisite), up to 1000 rows.
  • Autoloaded options (single site only): stored as the alloptions entry of the options group, with the raw option strings. get_option() unserializes each one as usual.

How each transient is stored#

Each transient has a value row and an optional timeout row, which can appear in any order. Docket Cache pairs them by name, then:

  • skips the transient if it has no value row or its timeout has passed
  • keeps its own timeout, limited to DOCKET_CACHE_MAXTTL, or uses 1 hour if it has none

The value row holds the serialized string WordPress wrote. Docket Cache converts it the same way get_option() would, while keeping files native wherever possible:

Database value Stored type On read
serialized, holding only arrays and stdClass array or object (native, unserialized with allowed_classes => ['stdClass']) native, from OPcache
serialized, holding any other class db_serialize (raw string) unserialized, when the class is loaded
double-serialized (a serialized string inside) string_serialize (the inner string) as is, same as get_option()
broken serialized data db_serialize (raw string) false, same as get_option()
not serialized string as is

Classes other than stdClass are not unserialized at copy time because, at the end of the delay phase, the class may not be loaded and would become __PHP_Incomplete_Class.

Format versions and upgrades#

sv Versions Change
none up to 26.04.09 serialized-looking strings unserialized on save and read
2 26.04.10 caller strings returned as is, key and group check added
3 26.04.11 and later database import stores values natively or as db_serialize

On read, files with an older sv are deleted and treated as a miss, and the caller rebuilds the value. After an update, each site refills its cache once. Because the drop-in version changes with each release, the delay phase also runs again and the database transients are copied in the current format.

Differences from WordPress without an object cache#

These behaviours are long-standing and worth knowing when debugging:

  • null is stored as ''.
  • A cached false is treated as a miss on the next request ($found is false).
  • A transient set to '' or [] reads back as false.
  • wp_cache_delete() returns true for a key that does not exist.
  • wp_cache_flush_group() returns false.
  • Very short expiries (a few seconds) may not be honoured exactly.
  • As with any persistent object cache, options keep their PHP type in the cache. An integer option is returned as an integer, not as the database string.

History#

Version Change
26.04.10 Caller strings returned exactly as stored (object injection fix). Stored key and group verified on read. sv 2.
26.04.11 Fixed a 26.04.10 regression where transients copied from the database came back as serialized strings, which crashed SEOPress PRO through update_plugins and WooCommerce through wc_attribute_taxonomies. The database import now stores arrays and stdClass natively and other classes as db_serialize, pairs value and timeout rows correctly, skips expired transients and keeps each transient's own expiry. Incrementing and decrementing reset the stored type. sv 3.