Which Laravel Octane features work only on Swoole or Open Swoole, and what happens to each if the app runs on FrankenPHP or RoadRunner?
answer
- four features need Swoole
- task workers and Swoole tables
- concurrently falls back to sequential
- octane store becomes a per-worker array
- Octane::table throws outright
basics
~20 sConcurrent tasks, ticks and intervals, the octane cache store and Swoole tables need Swoole or Open Swoole. Elsewhere Octane::concurrently runs tasks one after another, the octane store is a per-worker array, ticks never fire and Octane::table() throws.
solid answer
~40 sOctane's docs mark four features as Swoole-only, because they rely on Swoole **task workers** and **Swoole tables** (shared memory): `Octane::concurrently`, `Octane::tick`, the `octane` cache store with `interval()`, and custom tables via `Octane::table()`. Open Swoole provides the same set. On FrankenPHP or RoadRunner they degrade differently: `concurrently()` does not fail but falls back to `SequentialTaskDispatcher`, running the closures one after another in the same process; `Cache::store('octane')` becomes `OctaneArrayStore`, an in-process array per worker, and `interval()` just calls the resolver each time; ticks are never triggered, because the Swoole master's timer is what fires them; and `Octane::table()` throws "Tables may only be accessed when using the Swoole server." The quiet fallbacks are the dangerous part: code keeps working but loses its concurrency and its shared state.
code
php · 14 lines<?php
use Illuminate\Support\Facades\Cache;
use Laravel\Octane\Facades\Octane;
// On FrankenPHP or RoadRunner:
[$a, $b] = Octane::concurrently([
fn () => slowReportA(),
fn () => slowReportB(),
]); // runs A, then B, in this process
Cache::store('octane')->put('hits', 1, 60); // this worker's array only
Octane::table('example'); // throws: Swoole server requiredgo deeper
Recall the four Swoole-only features: concurrent tasks, ticks and intervals, the octane cache store, and tables.
Explain that they rest on task workers and Swoole tables, and what each does elsewhere: sequential tasks, a per-worker array store, silent ticks, a thrown exception.
Show you treat the quiet fallbacks as risks when switching servers and guard code that needs real concurrency or shared state.
Decide whether Swoole-only APIs are worth the server lock-in compared with queues, the scheduler and shared cache stores that work on any runtime.
## Why some features need Swoole **Laravel Octane** supports FrankenPHP, RoadRunner and Swoole (or Open Swoole) as servers. Most of Octane - booting the app once, resetting state per request, reload and stop commands - works the same on all of them. Four features do not, because they are built on two things only Swoole's server offers: - **Task workers** - a second pool of worker processes that the Swoole server can hand work to and wait on (`--task-workers`, `auto` by default). - **Swoole tables** - fixed-size tables in shared memory, readable and writable by every worker process on the server. | Feature | Built on | |---|---| | `Octane::concurrently([...])` | task workers (`taskWaitMulti`) | | `Octane::tick(...)` and cache `interval()` refreshes | the master's one-second timer dispatching a tick task | | `Cache::store('octane')` | a Swoole table sized by the `cache` config | | `Octane::table('name')` | Swoole tables defined in the `tables` config | Open Swoole is handled by the same driver (`swoole`); Octane checks for either extension and uses Open Swoole's table class when that extension is loaded. ## What happens on FrankenPHP or RoadRunner The behaviour is not uniform, and only one feature fails loudly. 1. **`Octane::concurrently`** picks a dispatcher at call time. Inside a Swoole server it uses the task workers. If the Swoole extension is merely installed (for example in an Artisan command on a Swoole host) it posts the tasks to the running Octane server over HTTP, falling back to sequential execution if it cannot connect. Otherwise it uses `SequentialTaskDispatcher`, which calls each closure **in turn, in the current process**. The result array looks the same; the time is the sum instead of the maximum. 2. **`Cache::store('octane')`** is registered as an `OctaneStore` over the Swoole cache table when that table exists, and as `OctaneArrayStore` - a subclass of Laravel's array store - when it does not. The array store lives in one worker's memory, so each worker has its own copy, and its `interval()` simply calls the resolver and returns the result every time. 3. **Ticks** are listeners on Octane's `TickReceived` event, which only the Swoole master's timer triggers. Registering `Octane::tick(...)` elsewhere does not error; the callback just never runs. 4. **`Octane::table('name')`** checks that a Swoole server is bound and throws an `Exception` with the message "Tables may only be accessed when using the Swoole server." otherwise. ## Why the quiet fallbacks matter - A dashboard that relied on `concurrently()` for speed gets slower after a server switch, with no error. - Code that used the `octane` store as a **shared** counter or cache across workers now keeps one per worker, so counts and invalidations diverge. - A tick that refreshed a value every few seconds silently stops, so data goes stale. These are exactly the bugs that appear when a team moves from Swoole to FrankenPHP or RoadRunner, or runs the test suite and queue workers outside Octane. ## Choosing whether to use them at all - Using these APIs ties the code to Swoole. Record that as a decision; changing servers later means rewriting those paths. - Laravel offers server-neutral alternatives for some jobs: queued jobs for background work, the scheduler for periodic tasks, a shared cache store for cross-worker data, and the `Concurrency` facade for parallel closures. Each is its own topic; the point here is that they do not depend on the Octane driver. - If you do use them, add a guard in code paths that must not degrade silently, for example checking that a Swoole server is bound before relying on shared state. ## Telling which path you are on Octane decides by checking the container at call time: inside a Swoole server, `Swoole\Http\Server` is bound, and that is what `Octane::table()` and the task dispatcher test for. Code that must not degrade can make the same check itself and fail fast, for example by throwing when a shared counter is requested outside a Swoole server. Logging the running driver at worker start, through a `WorkerStarting` listener, also makes a silent downgrade visible in the logs after a server change. ## A migration story A team runs an Octane app on Swoole, with a per-server request counter in the `octane` store and a tick that refreshes a price list every 30 seconds. They move to FrankenPHP for built-in HTTPS. Nothing errors. But the counter now shows a fraction of the traffic (one worker's share), the price list stops refreshing, and a dashboard using `concurrently()` is three times slower. Each symptom maps to one row of the table above. ## Quick reference - Swoole-only: concurrent tasks, ticks and intervals, the `octane` cache, tables. - Works everywhere: everything else Octane does. - Fails loudly elsewhere: `Octane::table()`. - Degrades quietly elsewhere: `concurrently()` (sequential), the `octane` store (per-worker array), ticks (never fire).
- Why does Octane::concurrently not throw on FrankenPHP?Octane's `tasks()` method chooses a dispatcher at call time: Swoole task workers inside a Swoole server, an HTTP hop to the Octane server when only the extension is present, and otherwise `SequentialTaskDispatcher`. The fallback keeps code portable across servers and in tests, at the cost of running the closures one after another.
- How would you catch an accidental dependency on Swoole-only behaviour?Run the relevant feature tests with Octane off, where the sequential and array fallbacks apply, and assert the behaviour you actually need, such as data shared across requests. For code that must have shared state, check that a Swoole server is bound and fail fast instead of relying on the fallback.
saying these in an interview costs you the question
- Octane::concurrently throws an exception on FrankenPHP
- The octane cache store is shared across workers on every Octane server
- Open Swoole lacks ticks and tables that Swoole has
- Ticks fall back to Laravel's scheduler when Swoole is absent
- Octane::table() silently returns an empty table outside Swoole