PHP compatibility and project lifecycle
The manual documents the built-in server in the CLI SAPI, a PHP 8.4.0 change to index file lookup, and forked workers since PHP 7.4.0 (not on Windows). PHP 8.5 behavior, support windows and licensing are unknown from the supplied source.
Official sources checked 2026-10-22. Browser lessons on this site run PHP 8.5.
This guide is based only on the official PHP manual page for the built-in web server.
What this project is
The built-in web server is a feature of the PHP CLI SAPI, started with the php -S option. It is part of PHP core tooling. It is not a framework adapter, an async library, a CMS or a separate interpreter. The manual says it is designed to aid application development and may also suit testing or demonstrations in controlled environments. It is not intended to be a full-featured web server and must not be used on a public network.
Its classification is a development server. Treat it as a convenient way to run PHP code over HTTP on your own machine, not as a production runtime.
Architecture
According to the manual, the server runs a single single-threaded process. If one request blocks, PHP applications stall. Since PHP 7.4.0 you can set the PHP_CLI_SERVER_WORKERS environment variable to fork several workers, which helps test code that needs concurrent requests. Multiple workers are not supported on Windows.
The source does not describe internals beyond this: no thread model, no memory limits, no timeouts and no performance characteristics. Those are unknown here, and this guide makes no speed claims.
Execution and content model
URI requests are served from the directory where PHP was started, unless the -t option names an explicit document root. If the URI does not name a file, index.php or index.html in that directory is returned. If neither exists, the lookup continues in parent directories until one is found or the document root has been reached. When an index file is found, $_SERVER['PATH_INFO'] is set to the trailing part of the URI. Otherwise the server returns 404. As of PHP 8.4.0, that index lookup also happens when the requested path looks like a file, meaning its last component contains a period, but the file cannot be found. Earlier versions returned 404 immediately.
If you pass a PHP file on the command line, it becomes a router script and runs at the start of each HTTP request. If it returns false, the requested resource is served as-is. Otherwise its output goes to the browser. The manual also lists the standard MIME types the server returns for common extensions, and shows a router pattern for setting a Content-Type for an unsupported extension.
Classic request lifetimes versus persistent workers
The documented model is that the router script runs at the start of each request, which matches the classic request-oriented way of thinking about PHP. Persistent-worker runtimes are different: they keep application code and objects in memory across many requests. That creates a risk of state leakage, such as static properties, global variables, singletons, caches, open connections or user data from one request showing up in the next. Such runtimes need explicit cleanup, resetting state between requests and recycling workers.
The manual page does not describe persistent application state for this server, so do not assume anything beyond the router-per-request statement. If your code depends on state being reset, verify it with your own tests. The PHP_CLI_SERVER_WORKERS option adds forked workers for concurrency testing; the source does not say it turns the server into a persistent-worker application runtime.
This server is also separate from Swoole and Open Swoole, which are distinct projects that this source does not cover. It is separate from Drupal as well: PHP core provides the language and this server, while a CMS product is an application you would run on top of some server.
Practical use cases
- Trying a script or a small router locally while learning PHP.
- Quick checks of routing logic with a router script.
- Demonstrations in controlled environments, as the manual allows.
- Reproducing concurrency problems with PHP_CLI_SERVER_WORKERS on non-Windows systems.
- Reusing a framework router script in development by checking for the cli-server SAPI name, as the manual describes.
Installation approach
There is nothing separate to install: you need a PHP CLI that includes the built-in server. Check with php --version and make sure it is PHP 8.4 for this learning track. PHP 8.5 is planned for the track but is not asserted here; compatibility with it is unknown from this source. No Composer packages are needed for the example below, and no container image is used because the source does not mention one.
Example in this guide
The example is a small native-host project with a router script, an entry file and one static file. It binds to 127.0.0.1 so only your machine can connect. It needs a local PHP CLI and a terminal, and it is not a browser exercise.
Strengths
- No web server configuration is needed to start.
- A router script gives explicit control over each request.
- The document root is selectable with -t.
- Standard MIME types are provided for many extensions.
- Optional forked workers help with concurrency testing on supported platforms.
Limitations
- Not for production or public networks, per the manual's repeated warnings.
- Single-threaded by default, so a blocked request stalls the app.
- Not a full-featured web server.
- Multiple workers are unsupported on Windows.
- The manual calls it an experimental feature not intended for production usage.
- Support window, license and performance data are unknown from the supplied source.
Deployment
There is no deployment recommendation for this server. The manual shows that php -S 0.0.0.0:8000 makes it reachable on all interfaces, then warns again that it should not be used on a public network. Follow that: keep development binds on 127.0.0.1 and use a proper web server stack for deployment. This server is not FastCGI, and FastCGI should never be exposed publicly either. This guide gives no hosting or cloud steps.
Links
The only source used is the official PHP manual page for the built-in web server, listed in the links.
Practical examples
These examples have not been run in a live environment yet. Commands and versions follow the official documentation linked on this page; check them before relying on them, and report a problem if one does not work.
Router script with php -S bound to localhost
A minimal native-host project: a router script decides whether to serve static files as-is (return false) or handle dynamic paths, run with the PHP CLI built-in server on 127.0.0.1 and a public document root. Requires a PHP 8.4 CLI and curl; development only.
router.php
declare(strict_types=1);
// Only meant for the CLI built-in server during development.
if (php_sapi_name() !== 'cli-server') {
http_response_code(500);
echo "This router is for the PHP built-in web server only.\n";
return true;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$path = is_string($path) && $path !== '' ? $path : '/';
$root = realpath(__DIR__ . '/public');
$candidate = realpath(__DIR__ . '/public' . $path);
// Serve existing non-PHP files from public/ as-is.
if (
$root !== false
&& $candidate !== false
&& str_starts_with($candidate, $root . DIRECTORY_SEPARATOR)
&& is_file($candidate)
&& strtolower(pathinfo($candidate, PATHINFO_EXTENSION)) !== 'php'
) {
return false;
}
if ($path === '/' || $path === '/index.php') {
require __DIR__ . '/public/index.php';
return true;
}
if ($path === '/health') {
header('Content-Type: text/plain; charset=utf-8');
echo "ok\n";
return true;
}
http_response_code(404);
header('Content-Type: text/plain; charset=utf-8');
echo "Not found: " . $path . "\n";
return true;
public/index.php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
echo "Hello from the PHP built-in web server\n";
echo 'SAPI: ' . php_sapi_name() . "\n";
echo 'PHP version: ' . PHP_VERSION . "\n";
public/hello.txt
Static file served as-is because router.php returned false.
Install and run
php --version
php -S 127.0.0.1:8000 -t public router.php
curl -i http://127.0.0.1:8000/
curl -i http://127.0.0.1:8000/hello.txt
curl -i http://127.0.0.1:8000/health
curl -i http://127.0.0.1:8000/missingExpected behavior
Run the server command from the project root (the directory containing router.php) and the terminal should report that it is listening on 127.0.0.1:8000. Requesting / should return plain text reporting the cli-server SAPI and the PHP version. /hello.txt should return the static file contents. /health should return ok. /missing should return a 404 with a short text message.
Development only: single-threaded by default, so a blocked request stalls everything. Do not bind to 0.0.0.0 or any public interface. Not a browser exercise; it needs a local PHP 8.4 CLI and curl. PHP 8.5 compatibility is unknown from the source.