Under Laravel Octane on Swoole, how does Cache::store('octane') store data, how does its interval() method refresh values, and what are the store's limits?
answer
- Swoole table shared by workers
- rows 1000, bytes 10000 by default
- gone on server restart
- interval refreshed on ticks
- no locks, oversized values throw
basics
~20 sThe octane store keeps serialized values in a Swoole table shared by all workers on one server, sized by cache rows (1000) and bytes (10000), and emptied on restart. interval() registers a resolver the tick refreshes every N seconds.
solid answer
~50 sOn Swoole, `Cache::store('octane')` is an `OctaneStore` over a Swoole table created when the server starts, with a `value` string column of `cache.bytes` (default 10000) and `cache.rows` (default 1000) rows from `config/octane.php`. Every worker on that server reads and writes the same table in shared memory, which makes it very fast, but it is per server and lost on restart. A serialized value longer than the column throws `ValueTooLargeForColumnException`. `Cache::store('octane')->interval('key', fn () => ..., seconds: 5)`, registered in a provider's `boot()`, stores the resolver in the table; on each tick Octane re-runs due resolvers and stores the result, and a read before the first refresh calls the resolver directly. The store does not implement Laravel's lock provider, and its `increment()` is a read followed by a write, so it is not a coordination tool across workers.
code
php · 21 lines<?php
namespace App\Providers;
use App\Support\FxClient;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Refreshed on ticks every 30 seconds, shared by all workers
Cache::store('octane')->interval('fx-rates', function () {
return app(FxClient::class)->latest();
}, seconds: 30);
}
}
// Anywhere in a request:
// $rates = Cache::store('octane')->get('fx-rates');go deeper
Recall that the octane cache store is a very fast, Swoole-only, in-memory cache that disappears on restart.
Explain the Swoole table behind it, the rows and bytes settings, the oversized-value exception, and how interval() refreshes through ticks.
Show you keep it for small per-server values, avoid it for locks and exact counters, and plan for restarts and fleet-wide inconsistency.
Weigh per-server shared memory against networked caches for each dataset, trading latency for consistency, durability and operational simplicity.
## What the `octane` store is On a Swoole server, **Laravel Octane** registers a cache driver named `octane`, reachable as `Cache::store('octane')`. It is backed by a **Swoole table**: a fixed-size table in shared memory that the Swoole server creates at start-up, before workers fork, so every worker process on that server sees the same data. Octane's docs describe it as able to reach up to two million operations per second, because no network or serialization to another process is involved beyond PHP's own `serialize()`. ```php Cache::store('octane')->put('framework', 'Laravel', 30); $value = Cache::store('octane')->get('framework'); ``` ## Sizing: the `cache` config ```php 'cache' => [ 'rows' => 1000, 'bytes' => 10000, ], ``` - `rows` - how many entries the table holds, fixed at server start. - `bytes` - the size of the `value` column, which stores the **serialized** value. Octane's table wrapper checks string lengths on write: a serialized value longer than `bytes` throws `Laravel\Octane\Exceptions\ValueTooLargeForColumnException` ("Value [...] is too large for [value] column."). A collection of a few hundred models can pass 10 KB easily. Changing either number needs a server restart, because the table is allocated once. ## Properties that follow from shared memory | Property | Consequence | |---|---| | per server | each server in a fleet has its own copy; there is no replication | | volatile | restarting the server empties it | | shared by workers | a value written by one worker is visible to all on that server | | fixed capacity | rows and bytes cannot grow at runtime | Expiry is stored per row and checked on read: an expired entry reads as missing. ## `interval()`: values that refresh themselves ```php Cache::store('octane')->interval('random', function () { return Str::random(10); }, seconds: 5); ``` Register intervals in a service provider's `boot()`. Under the hood: 1. `interval()` stores the resolver as a `SerializableClosure` under `interval-<key>`, with its refresh period and no last-refresh time; if another worker already registered it, it just remembers the key. 2. Octane listens for `TickReceived` and calls `refreshIntervalCaches()`, which re-runs every resolver whose period has elapsed and stores the result under the key. Errors are reported, not thrown. 3. `get('random')` returns the stored value; if nothing is stored yet, it calls the resolver directly. So intervals depend on **ticks**, which depend on the Swoole master's one-second timer and the task workers. `flush()` on the store removes everything except the `interval-` definitions. ## What the store is not 1. **Not a lock provider.** `OctaneStore` does not implement Laravel's `LockProvider`, so atomic locks are not available on it; use a store that supports them for mutual exclusion. 2. **Not an atomic counter.** `increment()` reads the row and writes a new value in two steps, so two workers incrementing at the same moment can lose an update. It is fine for approximate counters, not for balances or quotas. 3. **Not shared across servers or durable.** For data every server must agree on, or data that must survive deploys, use a networked store. 4. **Not available off Swoole.** On FrankenPHP or RoadRunner the `octane` store is a per-worker array store instead, and `interval()` simply calls the resolver each time. ## Example: an exchange-rate snapshot A pricing API converts currencies on almost every request. Rates change every few minutes and come from a remote service with a 200 ms latency. Registering `interval('fx-rates', ..., seconds: 30)` lets each server fetch the rates on its ticks and serve them from shared memory in microseconds; every worker on the server sees the same snapshot, and a restart simply triggers a fresh fetch. The payload must stay under `cache.bytes` once serialized, so the resolver should return a compact array of rates, not API response objects. ## When it fits - hot, small, read-mostly values each server can compute itself: feature-flag snapshots, exchange rates, configuration fetched from a remote service; - per-server rate hints or approximate counters where an occasional lost increment is acceptable. (Choosing among Laravel's cache stores in general is a separate topic.)
- Why can Cache::store('octane')->put() throw even though the key is short?The table's `value` column is fixed at `cache.bytes`, 10000 by default, and holds the serialized value. Octane checks the length on write and throws `ValueTooLargeForColumnException` when it is exceeded. Store smaller values, raise `bytes` and restart the server, or use another store for large payloads.
- Can two Octane servers behind a load balancer share the octane cache?No. Each Swoole server allocates its own table in its own shared memory. Values written on one server are invisible on the other, and a restart empties the table. For data every server must see, use a networked cache store.
saying these in an interview costs you the question
- The octane store replicates between servers behind a load balancer
- The octane cache survives octane:stop and a fresh start
- Cache::store('octane')->lock() gives safe cross-worker locking
- increment() on the octane store is atomic across workers
- Each worker process has its own copy of the octane cache on Swoole