Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

What Happens Inside a PHP Request: From Apache to Your Laravel Controller

About Post

Here's something that surprises developers coming to PHP from Node or Java: every single request, your Laravel app is built from scratch. The container, the config, the service providers, the routes. Built, used once, thrown away. Then built again for the next request.

That sounds wasteful. It's also one of the reasons PHP is so hard to break, and it explains most of the performance advice you've ever been given about Laravel.

Let's follow one request, GET /api/contracts/42, from the moment it reaches the server to the moment your controller runs, and back.

Stop 1: Apache decides who handles it

Apache receives the request and matches it to a virtual host by domain name. The vhost's DocumentRoot points at Laravel's public/ folder, which is why nothing else in your project is reachable from the web.

If the URL matches a real file, like /build/app.css, Apache serves it directly and PHP never wakes up. If not, the rewrite rules in public/.htaccess send the request to index.php. That's how one PHP file handles every route in your app: the "front controller" pattern.

Stop 2: the hand-off to PHP

Apache doesn't run your PHP code itself. There are two common ways it hands the work over:

  • mod_php embeds PHP inside each Apache process. Simple, common on older setups and local stacks, but every Apache process carries PHP's memory even when it's just serving an image.
  • PHP-FPM runs PHP as its own pool of worker processes. Apache (or Nginx) passes PHP requests to it over FastCGI. This is the standard for production today.
<FilesMatch \.php$>
    SetHandler "proxy:unix:/run/php/php8.4-fpm.sock|fcgi://localhost"
</FilesMatch>

The detail that matters in production: each FPM worker handles one request at a time. If pm.max_children allows 20 workers and 20 slow requests are running, request 21 waits in line. When a site "randomly" slows down under load, a full FPM pool is one of the first things I check. Slow external API calls inside requests are a common cause: the worker just sits there, waiting.

Stop 3: PHP compiles your code (unless OPcache already did)

PHP is interpreted, but not line by line. For each file it loads, PHP parses the source and compiles it into opcodes, low-level instructions for the Zend engine. Then it executes them.

A Laravel request touches a lot of files: your code, the framework, and everything in vendor/. Compiling all of that on every request would be painful. That's what OPcache is for: it keeps the compiled opcodes in shared memory, so after the first request, PHP skips the compile step entirely.

Two practical notes:

  • In production you can set opcache.validate_timestamps=0 so PHP stops checking whether files changed. Faster, but you must reload PHP-FPM after each deploy, or the server keeps running the old code.
  • The JIT that arrived in PHP 8.0 helps CPU-heavy work. For a typical web app that mostly waits on the database, OPcache is the big win.

Stop 4: Laravel boots

Now public/index.php runs. In Laravel 12 it's short (simplified here):

<?php

use Illuminate\Http\Request;

define('LARAVEL_START', microtime(true));

if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
    require $maintenance; // maintenance mode, before anything else loads
}

require __DIR__.'/../vendor/autoload.php';

$app = require_once __DIR__.'/../bootstrap/app.php';

$app->handleRequest(Request::capture());

Request::capture() wraps PHP's raw superglobals ($_GET, $_POST, $_SERVER) into a request object. Then the HTTP kernel runs its bootstrappers: load the environment, load config, set up error handling, register facades, then register and boot every service provider.

This is where the "rebuilt every request" cost lives. It's also why the classic production commands matter: php artisan config:cache replaces dozens of config files with one, route:cache skips building the route list, and php artisan optimize does the common caches in one go.

Stop 5: middleware, the onion

The request now passes through global middleware: trusting proxies, handling CORS, checking maintenance mode, trimming strings, turning empty strings into null. Think of middleware as layers of an onion. The request goes in through each layer, and the response comes back out through the same layers in reverse.

Stop 6: the router

The router finds the route matching GET /api/contracts/{contract}, then runs that route's middleware: the api group, authentication, rate limiting. Route model binding turns 42 into a Contract model, or a 404 if it doesn't exist.

Stop 7: finally, your controller

Laravel's service container builds your controller and injects its dependencies. If the method type-hints a Form Request, validation runs before your method body does. Your code queries the database, maybe dispatches a job, and returns something: a model, an array, a view or a resource.

Whatever you return gets converted into a proper response (arrays and models become JSON), travels back out through the middleware onion, and is sent to the client.

Stop 8: terminate, then forget everything

After the response is sent, terminable middleware gets a chance to run. Then PHP releases the request's objects and memory. The next request starts from a clean slate.

This is the "shared-nothing" model, and it's underrated. A memory leak can't build up over days. One request can't accidentally see another user's data left in a static variable. A crash affects one request, not the whole server.

What this means in practice: enable OPcache, cache config and routes on every deploy, keep service providers' boot() methods light, and size your FPM pool for your slowest requests, not your average ones.

And if booting every time is too slow?

That's what Laravel Octane is for. It boots the app once and keeps it in memory, serving many requests with FrankenPHP, Swoole or RoadRunner. It can be much faster, but you give up shared-nothing: static properties and singletons now survive between requests, and you have to write code with that in mind. For most apps, a well-tuned FPM setup with OPcache is plenty.

The official request lifecycle docs are a short, worthwhile read if you want the framework's own version of this tour.

Which stop on this trip has caused you the most trouble in production? My vote goes to Stop 2: a full FPM pool looks exactly like "the server is slow" until you check.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close