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:
wp_cache_set()and related functions, called by WordPress core and plugins at runtime.- Transients, which WordPress routes through the object cache (the
transientandsite-transientgroups). - 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-optionsandsite-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.
- 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. - 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. - 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, andDOCKET_CACHE_MAXTTL(default 4 days) for everything else. - Empty values.
''and[]are not written for the transient groups, and any existing file is removed. - Size limit. Values larger than DOCKET_CACHE_MAXSIZE (default 3MB, measured serialized) are not written.
- 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:
- Format version. A file with an
svlower than the current version is deleted and treated as a miss. This is how an update replaces old cache files without a manual flush. - Key and group. If the stored
keyorgroupdoes not match the request, it is a miss. - Expiry. A file past its
timeoutis deleted and treated as a miss. A file older thanDOCKET_CACHE_MAXTTLis deleted, but its value is still returned for this request. - Empty transients. A transient stored as
''is returned asfalse. - Unpack by type, see the next section.
- 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_serializeis created by Docket Cache from a native array it was given.db_serializeis created only from rows in the WordPress database, which WordPress itself unserializes inget_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()andset_site_transient()write a cache file in thetransientorsite-transientgroup.- 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 asfalse.
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:
- The object cache is off, and transient writes to the database are suppressed.
- At the end of the request, data is copied from the database into cache files.
- Transient rows that are expired or have no expiry are then deleted from the database. Rows with a future expiry are kept.
- 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 fromwp_options(andwp_sitemetaon multisite), up to 1000 rows. - Autoloaded options (single site only): stored as the
alloptionsentry of theoptionsgroup, 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:
nullis stored as''.- A cached
falseis treated as a miss on the next request ($foundisfalse). - A transient set to
''or[]reads back asfalse. wp_cache_delete()returnstruefor a key that does not exist.wp_cache_flush_group()returnsfalse.- 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. |