How to Use Drupal’s State API for Temporary Data Storage
Drupal applications often need to remember a small piece of information without treating it as website configuration. Examples include the last successful synchronisation time, whether a maintenance task has completed, or an identifier returned by an external service. Storing these values in content fields or configuration creates unnecessary complexity.
Drupal’s State API provides a simple key-value storage service for temporary, environment-specific data. A value can survive cache clears and normal requests, while remaining separate from exported configuration and content that editors manage.
The API is available in modern Drupal versions, including Drupal 9, 10 and 11. It is useful for custom modules, update hooks, cron jobs, administrative forms and integrations that need a small amount of persistent application state.
For Australian websites, this can be practical when a Melbourne retailer synchronises products with a warehouse system, a Brisbane organisation records the last import from a government feed, or a Sydney publisher tracks a scheduled content task. The important point is to use State API for operational information rather than business records or personal data.
What The State API Stores
State API stores values as key-value pairs in Drupal’s database. Internally, the values belong to the state collection in the key-value storage system, commonly represented by the key_value database table. Drupal serialises PHP values, so a state value can be a string, integer, Boolean, array or another serialisable value.
State is persistent, but it is intended to be temporary or changeable application data. “Temporary” does not mean that Drupal automatically deletes it after a particular period. A state value remains until your code changes or removes it, or an administrator clears it through an appropriate maintenance process.
Typical examples include mymodule.last_import, mymodule.api_version, or mymodule.feed_enabled. Prefixing keys with the module name reduces the chance of collisions with core or contributed modules.
State is local to the Drupal installation and environment. A value created on a production site will not automatically appear in a development database, and it is not included in configuration synchronisation. This makes it suitable for environment-specific details such as a local lock, an integration status flag or the timestamp of the last successful operation.
State API Versus Configuration And Content
Configuration represents the intended setup of a Drupal site. Site name, enabled modules, views, roles and field definitions are examples of configuration. Configuration can be exported to YAML, reviewed in version control and imported into another environment. State should not be used for values that need to move from development to staging and production.
Content represents information managed by the website, such as products, news articles, event records and customer-facing pages. If a value needs editorial control, revision history, translation, access permissions or a relationship to an entity, it usually belongs in content or a custom entity rather than state.
State is a better fit for values generated by the application. A module may record the last time it checked a remote endpoint, whether it has displayed a one-time warning, or which cursor it should use for the next import. These values support processing but are not generally part of the website’s public information model.
State should also be distinguished from cache storage. Cache data can be deleted at any time, and code must be able to rebuild it. State remains available until explicitly changed. If losing the value would merely make the next request slower, use cache. If the value represents a small operational checkpoint, State API may be appropriate.
Reading And Writing Values
For a quick operation inside a procedural function, Drupal’s service locator can access the state service:
$state = \Drupal::state();
$last_run = $state->get('mymodule.last_run', 0);
$state->set('mymodule.last_run', \Drupal::time()->getRequestTime());
The second argument to get() is a default value. It is returned when the key does not exist, which avoids warnings and makes first-run behaviour explicit. A timestamp can therefore default to 0, while a Boolean setting might default to FALSE.
State values can be deleted when they are no longer useful:
\Drupal::state()->delete('mymodule.last_run');
Drupal also supports multiple operations:
$state = \Drupal::state();
$values = $state->getMultiple([
'mymodule.last_run',
'mymodule.item_count',
]);
$state->setMultiple([
'mymodule.last_run' => \Drupal::time()->getRequestTime(),
'mymodule.item_count' => 42,
]);
Use consistent value types. If a key stores an integer timestamp, do not sometimes save a formatted date string. Predictable types make comparisons, testing and future maintenance much easier.
Injecting State Into A Custom Service
Dependency injection is the preferred approach for services, plugins and controllers. It makes the dependency visible and allows tests to replace it with a mock or another implementation.
A service class can receive StateInterface through its constructor:
namespace Drupal\mymodule;
use Drupal\Core\State\StateInterface;
use Drupal\Component\Datetime\TimeInterface;
final class ImportTracker {
public function __construct(
private readonly StateInterface $state,
private readonly TimeInterface $time,
) {}
public function markCompleted(int $count): void {
$this->state->setMultiple([
'mymodule.last_import' => $this->time->getRequestTime(),
'mymodule.import_count' => $count,
]);
}
public function getLastImport(): int {
return (int) $this->state->get('mymodule.last_import', 0);
}
}
Register the class in mymodule.services.yml:
services:
mymodule.import_tracker:
class: Drupal\mymodule\ImportTracker
arguments:
- '@state'
- '@datetime.time'
The state service implements Drupal\Core\State\StateInterface. Injecting datetime.time as well avoids calling the system clock directly, which makes time-dependent tests more reliable.
Procedural access through \Drupal::state() is acceptable in hooks, update functions or small scripts where dependency injection is not practical. It should not become the default in reusable object-oriented code, because service location hides dependencies and makes testing less convenient.
Using State In Cron And Integrations
Cron tasks are a common State API use case. A module can read the last successful run, process records after that point, and update the value only when the operation finishes successfully:
function mymodule_cron(): void {
$state = \Drupal::state();
$last_run = (int) $state->get('mymodule.last_run', 0);
$processed = mymodule_process_items_since($last_run);
if ($processed !== FALSE) {
$state->setMultiple([
'mymodule.last_run' => \Drupal::time()->getRequestTime(),
'mymodule.item_count' => (int) $processed,
]);
}
}
Updating the timestamp after successful processing is important. If the value is written before the remote request or database operation completes, a failure may cause records to be skipped on the next run. For larger imports, a queue, migration or dedicated tracking entity may be safer than one global state value.
State does not provide a lock automatically. Two cron processes could read the same value and start the same work. Use Drupal’s lock service when concurrent execution is possible:
$lock = \Drupal::service('lock');
if ($lock->acquire('mymodule.import', 300)) {
try {
// Perform the import.
}
finally {
$lock->release('mymodule.import');
}
}
A state value can record progress, but it should not be treated as a transaction log. If an Australian retailer relies on an overnight stock feed before opening Sydney or Perth stores, the integration should also log failures and expose a recoverable process. State alone is too small and opaque for detailed operational history.
Security, Privacy And Deployment Considerations
Do not use State API for passwords, API secrets, access tokens or other sensitive credentials. State values are stored in the database and may be visible to database administrators, backups, deployment operators or debugging tools. Use Drupal’s Key module, environment variables, a secrets manager or another purpose-built solution for credentials.
Personal information also requires care. Under Australia’s Privacy Act 1988 and the Australian Privacy Principles, organisations need to handle personal information appropriately, including protecting it from misuse, loss and unauthorised access. A customer email address, address, health detail or identifiable support note should not be placed in state merely because it is convenient.
State is not exported with configuration. A deployment that imports configuration on a new environment will not recreate state values. Provide sensible defaults and an administrative rebuild or initialisation path when the module requires a value. If the value must be transferred between environments, it is probably configuration, content, deployment data or a dedicated integration record instead.
Database backups may include state values, and managed hosting arrangements can affect where those backups are stored. An organisation operating across Australia should check its hosting, retention and privacy requirements rather than assuming that a database value is harmless because it is not displayed on a page.
Checking State Values During Maintenance
State API values can be inspected through code, Drush commands, database tools or a custom administrative form. A small status report is often more useful than exposing raw values. For example, an administrative page could show the last import as a human-readable date, the number of records processed and whether the integration has ever completed.
When displaying a timestamp, format it using Drupal’s date and time services so the site’s configured timezone is respected. This matters for teams working across Australian Eastern, Central and Western time zones, especially when cron runs near daylight-saving changes in New South Wales, Victoria, Tasmania and the Australian Capital Territory.
Test the first-run case, a normal update, a failed operation and a reset. A module should behave sensibly when the key is absent, contains an old value or has been deleted during maintenance. Avoid assuming that a state key always exists in a freshly installed or restored database.
The following distinctions help determine which Drupal storage mechanism fits a value:
| Storage mechanism | Best suited to | Exported with configuration | Can be cleared automatically | Example |
|---|---|---|---|---|
| State API | Small, environment-specific operational values | No | No | Last successful feed import |
| Configuration | Site settings and intended behaviour | Yes | No | API endpoint or feature setting |
| Cache | Rebuildable temporary results | No | Yes | Rendered response or lookup result |
| Content or entity | Editorial and business records | No | No | Product, article or event |
| Key or secrets manager | Credentials and confidential secrets | No | Depends on provider | API token or private key |
Practical Rules For Reliable State Storage
State API is deliberately small and straightforward, but good naming and lifecycle decisions prevent many problems. Use module-prefixed keys, document what each value means, and define a default for every read. Keep stored data compact and avoid turning one key into an unstructured collection of unrelated information.
For a custom module, these practices provide a dependable baseline:
- Use State API for application checkpoints, flags and environment-specific runtime information.
- Use configuration for settings that administrators change or deployments must share.
- Use cache for data that can be regenerated without losing business information.
- Protect credentials and personal information with an appropriate secure storage design.
- Delete obsolete keys during module uninstall or a controlled update process.
A clear storage choice makes Drupal maintenance easier. When a site editor changes a setting, it belongs in configuration; when an integration remembers its last completed operation, state is usually the right tool; when a record must be audited, queried or exposed to users, it belongs in a proper entity or database structure.