Application Lifecycle

To use Fast well, it helps to picture what actually happens when you run serve(). It is different from classic PHP, and once the shape clicks, a lot of the framework's design decisions start to make sense.

The big picture

When you start a Fast server, you get a small tree of processes:

Master (binds the socket, forks and supervises)
  └── Worker × N   (bootstraps your app once, runs an event loop)
        └── Loop (stream_select) ── Scheduler (fibers)
              └── Http\Server ── one fiber per connection
                    └── Application ── middleware → router → handler

The master process binds the listening socket and forks a pool of workers. It does not serve requests itself; its whole job is supervision. If a worker dies, the master respawns it.

Each worker bootstraps your application a single time, then settles into an event loop. Every incoming connection gets its own fiber, so one worker can juggle many connections at once without threads and without blocking.

Boot once, serve forever

This is the heart of what makes Fast fast. In traditional PHP, the sequence for every request is: start PHP, load your framework, wire up services, handle the request, throw everything away. Fast does the expensive part exactly once, when a worker starts, and then reuses that warm application for every request the worker handles.

Because of that, there is an important rule:

Register all of your routes, middleware, and services before you call serve(). Once serve() runs, the application graph is built and forked into the workers.

require __DIR__ . '/bootstrap.php';

use_views(__DIR__ . '/views');
use_session(new Fast\Session\SessionConfig(key: $key));

get('/', fn (): string => render('home'));

serve(port: 8080, workers: 4);   // everything above is already wired up

Where per-request state lives

If the application lives for thousands of requests, how do you avoid one request's data leaking into the next? Fast solves this with container scopes.

Every request runs inside a fresh child scope of the container. Services you register as scoped are built once for that request and released the moment the response is sent. Singletons, on the other hand, are built once per worker and shared. So you keep stateless, reusable things (a database handle, the view service) as singletons, and anything request-specific as scoped, and the framework guarantees the request state never survives past the response.

You will see this play out in detail in the Container & Services section.

Lazy by default

Fast will not open a database connection, fork a key-value shard, or build a service until something actually needs it. Enabling a subsystem with a use_* call only registers lazy wiring; the real work happens on first use inside a worker. That is why a route that never touches the database pays nothing for the database layer being enabled.

Startup and shutdown hooks

Sometimes you need to run code once when a worker boots, or once as it drains. Two hooks cover that:

on_start(function (): void {
	// Runs once per worker, right after the app is built.
});

on_shutdown(function (): void {
	// Runs once per worker during a graceful drain.
});

Use on_start for per-worker warmup and on_shutdown for cleanup. Both run inside the worker, off the request path.

With the lifecycle in mind, let us look at how a Fast project is typically laid out in Directory Structure.