Octane Worker N+1 Queries &amp; Stale State Fixes | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Laravel Octane + FrankenPHP: Eliminating N+1 Queries in Long-Lived Worker Processes        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)

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

  #laravel   #octane   #eloquent   #performance   #frankenphp  

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

     20 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

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

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-octane-frankenphp-eliminating-n1-queries-in-long-lived-worker-processes&text=Laravel+Octane+%2B+FrankenPHP%3A+Eliminating+N%2B1+Queries+in+Long-Lived+Worker+Processes) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-octane-frankenphp-eliminating-n1-queries-in-long-lived-worker-processes) 

 Frequently Asked Questions 
----------------------------

  3 questions  

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

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

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

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/260/8c84f424e42da01993c9ba4b8eb19655.png) laravel ai agents 

### Laravel AI SDK: Tool-Calling Agents and Conversation Persistence

Build reliable tool-calling AI agents in Laravel using the Prism package. Learn how to wire tools, persist con...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-ai-sdk-tool-calling-agents-and-conversation-persistence) [ ![Laravel Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://cdn.msaied.com/259/e8ce445f021c2b26ebe4dd5da50014f8.png) livewire laravel alpine 

### Laravel Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

Go beyond the docs: understand how Livewire v3 diffs the DOM with morph markers, intercept the lifecycle with...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration) [ ![Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging](https://cdn.msaied.com/258/673a80fa8e42ae375a4bba21bdcd92ea.png) laravel packages service-providers 

### Laravel Package Development: Service Providers, Auto-Discovery, and Config Merging

Build a production-ready Laravel package from scratch — covering service provider design, auto-discovery via c...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 21 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/laravel-package-development-service-providers-auto-discovery-and-config-merging-1) 

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

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
