skip to content

Octane

Octane serves Laravel from long-lived FrankenPHP, Swoole or RoadRunner workers instead of booting per request. Interviewers probe the catch: state that used to die now survives.

on this pageshow

explore

questions

18

Which Laravel Octane features work only on Swoole or Open Swoole, and what happens to each if the app runs on FrankenPHP or RoadRunner?

level: juniorimportance: must knowfreq 30%

answer

  1. four features need Swoole
  2. task workers and Swoole tables
  3. concurrently falls back to sequential
  4. octane store becomes a per-worker array
  5. Octane::table throws outright

basics

~20 s

Concurrent 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 s

Octane'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
<?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 required

go deeper

for a junior

Recall the four Swoole-only features: concurrent tasks, ticks and intervals, the octane cache store, and tables.

for a middle

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.

for a senior

Show you treat the quiet fallbacks as risks when switching servers and guard code that needs real concurrency or shared state.

for a principal

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
open as a page

In Laravel Octane, what do octane:install and the OCTANE_SERVER variable decide, and how does octane:start choose which server to launch?

level: juniorimportance: must knowfreq 45%

basics

~10 s

octane:install asks for FrankenPHP, RoadRunner or Swoole, sets up what that server needs, publishes config/octane.php and writes OCTANE_SERVER to .env. octane:start launches --server if given, otherwise config('octane.server'), on 127.0.0.1:8000.

open as a page

Under Laravel Octane, how often do service providers' register() and boot() methods run, and why is reading request() or auth()->user() inside boot() a bug?

level: juniorimportance: must knowfreq 50%

basics

~20 s

Under Octane, providers' register() and boot() run once per worker, at boot. The request then is a synthetic GET built from app.url and nobody is logged in, so whatever boot() reads from request() or auth() is wrong for real requests.

open as a page

After deploying new code to a Laravel Octane server, why do you run octane:reload, and when do you need a full restart instead?

level: middleimportance: must knowfreq 55%

basics

~20 s

Octane workers keep the old code in memory, so a deploy is ignored until they restart; octane:reload restarts them gracefully inside the running server. Options fixed at start - port, worker count, max_execution_time, the driver - need a stop and start.

open as a page

After moving a Laravel banking API to Octane, some responses show the previous customer's accounts; which Octane-specific state is carrying them over, and how do you fix it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Octane clones one booted app per request, but instances resolved at boot or warmed, and static properties, are shared by every request on the worker. A singleton or static cache that memoizes the current customer serves them to the next request.

open as a page

Under Laravel Octane on Swoole, how do you fetch three dashboards' data in parallel with Octane::concurrently, and where do those closures actually run?

level: middleimportance: should knowfreq 30%

basics

~20 s

Pass Octane::concurrently an array of closures, one per dashboard query, and destructure the returned array; keys are preserved. Each closure is serialized and runs in a Swoole task worker process, so capture only serializable values like IDs.

open as a page

When a Laravel Octane app sits behind an Nginx proxy that terminates TLS, why can it generate http:// links, and what do OCTANE_HTTPS and the proxy config fix?

level: middleimportance: should knowfreq 35%

basics

~20 s

Nginx talks plain HTTP to Octane on 127.0.0.1:8000, so Laravel sees an http request and builds http:// URLs. OCTANE_HTTPS=true makes Octane force the https scheme on every request; Nginx serves static files and proxies the rest to Octane.

open as a page

Under Laravel Octane in local development, why does an edited route not show up until a restart, and what does octane:start --watch need to fix that?

level: middleimportance: should knowfreq 35%

basics

~20 s

Octane workers boot the app once and keep it in memory, so edited files are not reread. octane:start --watch runs a Node chokidar watcher over config('octane.watch') and reloads the workers on change; FrankenPHP uses its own watcher instead.

open as a page

In Laravel Octane, what do the --max-requests option and the garbage setting do about memory growth, and why are neither of them a fix for a leak?

level: middleimportance: should knowfreq 35%

basics

~10 s

--max-requests (default 500) gracefully replaces a worker after that many requests. garbage (50 MB) is read only by the CollectGarbage listener, which ships commented out. Both contain memory growth; neither removes a leak.

open as a page

In Laravel Octane, how do you reset your own static state before each request through the listeners config, and which events can you hook?

level: middleimportance: should knowfreq 25%

basics

~20 s

Add a listener class under RequestReceived in the listeners array of config/octane.php, after Octane's own spread entries; its handle($event) runs before each request and can reset statics. Other hooks include RequestTerminated, OperationTerminated, WorkerStarting and WorkerStopping.

open as a page

Under Laravel Octane on Swoole, what happens when a task in Octane::concurrently throws an exception or runs past the wait time, and how should the caller handle it?

level: seniorimportance: should knowfreq 15%

basics

~20 s

A task's exception is rethrown in the caller as Laravel\Octane\Exceptions\TaskException with the original class name, message, file and line. A failed wait (3000 ms default) throws TaskTimeoutException; a task missing from the results comes back as false.

open as a page

For a high-traffic API gateway on Laravel Octane, how do you choose between FrankenPHP, Swoole and RoadRunner, and how many workers do you start?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Swoole or Open Swoole if you need Octane's tasks, ticks, cache or tables; otherwise FrankenPHP (one Go binary with built-in HTTPS) or RoadRunner (a Go process manager). Size --workers from measured memory and I/O wait, not cores alone.

open as a page

Under Laravel Octane, why can a singleton that received the container, the request or the config repository in its constructor see stale values, and what are the fixes?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A singleton built at boot keeps the objects it was given, while Octane gives each request a new request, a cloned config and a cloned container. Inject resolver closures, use app(), request() and config(), or pass values to methods.

open as a page

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?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

The 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.

open as a page

Under Laravel Octane on Swoole, how does Octane::tick() with seconds() and immediate() schedule a callback, and where does that callback run?

level: middleimportance: nice to knowfreq 15%

basics

~10 s

Octane::tick('name', $callback)->seconds(10), registered in a provider's boot(), listens for TickReceived. The Swoole master sends a tick task each second and a task worker runs due callbacks. tick() defaults immediate to true.

open as a page

In Laravel Octane's config/octane.php, what do the warm and flush arrays do, and when would you add your own binding to either?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

warm lists bindings Octane resolves once at worker boot, shared by later requests; flush lists bindings Octane forgets after every request, so they are rebuilt next time. Warm only stateless services; flush ones holding per-request state.

open as a page

Under Laravel Octane on Swoole, how do you define and use a custom table through the tables config and Octane::table(), and what constraints does it impose?

level: seniorimportance: nice to knowfreq 10%

basics

~10 s

Declare the table in config/octane.php's tables array as 'name:rows' with string:size, int or float columns, then use Octane::table('name')->set($key, [...]) and get($key). It is shared by all workers, fixed-size and lost on restart.

open as a page

In Laravel Octane, what does max_execution_time in config/octane.php do, and how does each server enforce it on a slow request?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

max_execution_time caps how long Octane lets one request run: 30 seconds by default, 0 for no limit, fixed when the server starts. Swoole kills the stuck worker and answers 408, RoadRunner gets it as exec_ttl, FrankenPHP applies set_time_limit on Linux.

open as a page