Octane Worker N+1 Queries &amp; Stale State Fixes | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com/public) - [Home](https://msaied.com/public)
- [Projects](https://msaied.com/public/projects)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [About](https://msaied.com/public#about)

           [  Contact](https://msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com/public)
- [ProjectsCase studies](https://msaied.com/public/projects)
- [ArticlesEngineering notes](https://msaied.com/public/articles)
- [CertificatesCredentials](https://msaied.com/public/certificates)
- [AboutHow I work](https://msaied.com/public#about)
- [ContactGet in touch](https://msaied.com/public#contact)

  [Start a conversation](https://msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com/public)
2. /
3. [Articles](https://msaied.com/public/articles)
4. /
5. Laravel Octane + FrankenPHP: Eliminating N+1 Queries in Long-Lived Worker Processes

 Laravel Octane + FrankenPHP: Eliminating N+1 Queries in Long-Lived Worker Processes
====================================================================================

 Long-lived Octane workers expose a class of N+1 and stale-state bugs that never surface in traditional PHP-FPM apps. Learn how to detect, fix, and guard against them in production.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com/public#person) Published 20 Jun 2026 · Updated 20 Jun 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Octane + FrankenPHP: Eliminating N+1 Queries in Long-Lived Worker Processes](https://cdn.msaied.com/248/b37c48bf022a60aced7b2c47259fc977.png) 

  On this page +1. [The Problem Nobody Talks About: Octane Changes When N+1 Bites You](#the-problem-nobody-talks-about-octane-changes-when-n1-bites-you)
2. [Why Octane Amplifies N+1 Pain](#why-octane-amplifies-n1-pain)
3. [Detecting N+1 in Worker Context](#detecting-n1-in-worker-context)
4. [Fixing the Root Cause: Eager Loading with Constraints](#fixing-the-root-cause-eager-loading-with-constraints)
5. [Stale Relation Caches: The Octane-Specific Hazard](#stale-relation-caches-the-octane-specific-hazard)
6. [Covering Indexes to Absorb the Remaining Queries](#covering-indexes-to-absorb-the-remaining-queries)
7. [Takeaways](#takeaways)

 The Problem Nobody Talks About: Octane Changes When N+1 Bites You
-----------------------------------------------------------------

In a classic PHP-FPM setup, every request boots a fresh process. Stale Eloquent relations, forgotten eager-loads, and singleton state are all silently reset. Move to Laravel Octane (whether on Swoole, RoadRunner, or FrankenPHP worker mode) and that safety net disappears.

The same N+1 query that was invisible under FPM — because each request paid the boot cost anyway — now compounds across thousands of requests in the same worker, and stale relation caches from request A bleed into request B.

### Why Octane Amplifies N+1 Pain

Consider a typical resource controller:

```php
// OrderController.php
public function index(): JsonResponse
{
    $orders = Order::all(); // no eager load
    return response()->json(
        $orders->map(fn($o) => [
            'id'       => $o->id,
            'customer' => $o->customer->name, // N+1 here
            'items'    => $o->items->count(),  // N+1 here
        ])
    );
}

```

Under FPM this fires `1 + N + N` queries per request. Under Octane it fires the same count, but the worker handles hundreds of requests per second — the database connection pool saturates fast and latency spikes.

### Detecting N+1 in Worker Context

Use `DB::listen` scoped to a request lifecycle, not a global listener that accumulates across requests:

```php
// AppServiceProvider::boot()
if (app()->environment('local')) {
    $this->app->make('events')->listen(
        RequestHandled::class,
        function () {
            // Telescope or a custom collector resets here
        }
    );
}

```

Better: install **Laravel Telescope** and enable the `QueryWatcher`. With Octane, make sure you flush Telescope's request context on `octane:request-terminated`:

```php
// OctaneServiceProvider or a dedicated listener
use Laravel\Octane\Events\RequestTerminated;

Event::listen(RequestTerminated::class, function () {
    app(\Laravel\Telescope\Telescope::class)->flushEntries();
});

```

Without this, Telescope accumulates all queries from every request in the worker's lifetime into a single "request" entry — useless for diagnosis.

### Fixing the Root Cause: Eager Loading with Constraints

```php
$orders = Order::with([
    'customer:id,name',          // select only what you need
    'items' => fn($q) => $q->select('id','order_id'), // constrained
])->latest()->paginate(50);

```

For deeply nested relations that vary by context, use **lazy eager loading** defensively:

```php
$orders->loadMissing('customer', 'items');

```

`loadMissing` skips already-loaded relations, which matters in Octane because a singleton service might cache a partially-loaded collection across requests.

### Stale Relation Caches: The Octane-Specific Hazard

If you store an Eloquent model in a singleton (common in multi-tenant resolvers), its loaded relations persist across requests:

```php
// Dangerous singleton pattern
app()->singleton(CurrentPlan::class, function () {
    return Plan::with('features')->find(config('plan.id'));
});

```

Request 1 loads `Plan` with `features`. Request 2 gets the cached instance — but if features changed in the DB, the stale relation is served silently.

**Fix:** scope singletons to the request, not the worker:

```php
app()->scoped(CurrentPlan::class, function () {
    return Plan::with('features')->find(config('plan.id'));
});

```

`scoped()` (added in Laravel 9) re-resolves the binding on each Octane request cycle, giving you singleton-like performance within a request while staying fresh across requests.

### Covering Indexes to Absorb the Remaining Queries

After eager loading, profile the queries that remain. For paginated list queries, a covering index eliminates heap fetches:

```sql
CREATE INDEX idx_orders_customer_created
    ON orders (customer_id, created_at DESC)
    INCLUDE (id, status, total);

```

Laravel's `EXPLAIN` helper makes this easy to verify:

```php
DB::table('orders')
    ->where('customer_id', 42)
    ->orderByDesc('created_at')
    ->explain()
    ->dd();

```

Look for `Index Only Scan` (PostgreSQL) or `Using index` (MySQL) in the output.

### Takeaways

- **Octane doesn't create N+1 bugs — it reveals and amplifies them.** Fix eager loading first.
- Use `scoped()` instead of `singleton()` for anything that touches the database or tenant state.
- Flush Telescope/Debugbar context on `RequestTerminated` or your diagnostics are meaningless.
- Covering indexes are the last line of defence once queries are correctly structured.
- Profile under realistic concurrency, not a single sequential request, to see Octane's true query pressure.

- [laravel](https://msaied.com/public/articles?search=laravel)
- [octane](https://msaied.com/public/articles?search=octane)
- [eloquent](https://msaied.com/public/articles?search=eloquent)
- [performance](https://msaied.com/public/articles?search=performance)
- [frankenphp](https://msaied.com/public/articles?search=frankenphp)

 Frequently asked questions 
---------------------------

  Does `scoped()` have a performance cost compared to `singleton()` in Octane?Minimal. `scoped()` resolves the binding once per Octane request cycle and caches it for the duration of that request, so within a single request it behaves identically to a singleton. The re-resolution overhead is negligible compared to a database query.

   Will Laravel Debugbar work correctly with Octane to show per-request query counts?Not out of the box. Debugbar accumulates data in a static collector that persists across Octane requests. You need to listen to the `RequestTerminated` event and call `Debugbar::disable()` or reset its collectors, or switch to Telescope with proper Octane flushing.

   Is `loadMissing` safe to call on a collection that was cached in a scoped binding?Yes. `loadMissing` checks the in-memory relation cache on each model and only fires queries for relations not yet loaded. In a scoped binding the model is fresh per request, so `loadMissing` behaves predictably without risk of serving stale data.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleAdvanced Authorization in Laravel: Gates, Policies, and Response-Based Access Control](https://msaied.com/public/articles/advanced-authorization-in-laravel-gates-policies-and-response-based-access-control) [Next articleDatabase Performance: Partial Indexes, Covering Indexes, and Query Plan Analysis in Laravel](https://msaied.com/public/articles/database-performance-partial-indexes-covering-indexes-and-query-plan-analysis-in-laravel)  

   On this page
-------------

1. [The Problem Nobody Talks About: Octane Changes When N+1 Bites You](#the-problem-nobody-talks-about-octane-changes-when-n1-bites-you)
2. [Why Octane Amplifies N+1 Pain](#why-octane-amplifies-n1-pain)
3. [Detecting N+1 in Worker Context](#detecting-n1-in-worker-context)
4. [Fixing the Root Cause: Eager Loading with Constraints](#fixing-the-root-cause-eager-loading-with-constraints)
5. [Stale Relation Caches: The Octane-Specific Hazard](#stale-relation-caches-the-octane-specific-hazard)
6. [Covering Indexes to Absorb the Remaining Queries](#covering-indexes-to-absorb-the-remaining-queries)
7. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/745/8744e1be5136b430da52e9fca3ed3964.png)  · 3 min read### Service Container Deep Dive: Contextual Binding, Tagging, and Method Injection

6 Oct 2026 ](https://msaied.com/public/articles/service-container-deep-dive-contextual-binding-tagging-and-method-injection-1) [ ![](https://cdn.msaied.com/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com/public)
- [Articles](https://msaied.com/public/articles)
- [Certificates](https://msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/public/sitemap.xml)
