PHP Runtime And Server Environment
The C10K Problem And Nginx
A server can have many idle or slow clients connected without doing much CPU work. It can also have fewer clients that each trigger expensive database queries, file processing, or PHP application logic. Those are different bottlenecks. The C10K problem was mostly about connection concurrency, socket readiness, kernel limits, memory use, and scheduling overhead. It was not a promise that every application behind the web server can do ten thousand expensive dynamic requests at once.
Nginx became popular partly because its event-driven worker model handles large numbers of connections efficiently. For PHP applications, nginx is usually the front web server that accepts HTTP connections, serves static files, terminates or forwards TLS depending on the deployment, applies routing rules, and passes PHP requests to PHP-FPM over FastCGI. That solves one class of connection-management problem, but it does not remove PHP-FPM capacity planning.
What C10K Really Means
C10K means many simultaneous clients. It does not necessarily mean ten thousand requests actively executing PHP code at the same instant. A client might be connected but waiting. A browser might hold keep-alive connections. A slow mobile client might receive a response gradually. A reverse proxy might keep upstream and downstream connections open. A chat, streaming, or long-polling app might keep connections around for longer than a normal page request.
Older server designs often used one process or one thread per connection. That can be simple to reason about, but it becomes expensive as the number of concurrent connections grows. Each process or thread consumes memory and scheduler attention. Thousands of mostly idle clients can still create thousands of execution contexts.
The C10K discussion pushed server design toward non-blocking I/O, readiness notification, event loops, efficient kernel APIs, and careful resource limits. Instead of dedicating one thread to one mostly idle client, an event-driven server can let a worker process watch many sockets and act only when a socket is ready for useful work.
For a PHP developer, the important lesson is not the exact number ten thousand. The important lesson is that connection count, request rate, response time, memory, open files, upstream capacity, and application work must be measured separately.
How Nginx Approaches The Problem
Nginx uses a small number of worker processes and event-driven connection handling. A worker can manage many connections by reacting to events such as a socket becoming readable or writable. The operating system tells nginx which connections need attention, and nginx performs small pieces of work without assigning a dedicated thread to every connected client.
This is why nginx can sit in front of applications as a reverse proxy and web server. It is good at holding client connections, serving static assets, buffering where configured, applying request limits, forwarding dynamic work to upstream services, and keeping the edge of the application responsive under normal load.
Nginx configuration exposes this model through directives such as worker processes, worker connections, event processing methods, file descriptor limits, logs, keep-alive behavior, proxy timeouts, FastCGI settings, and buffering settings. These settings are not magic switches. They describe how much work nginx may attempt and how it interacts with the operating system and upstream services.
A small conceptual configuration shape is:
worker_processes auto;
events {
worker_connections 2048;
}
http {
server {
listen 443 ssl;
root /var/www/app/public;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
}
This is not a full production configuration. It is a shape for discussion: nginx handles the client-facing HTTP layer and passes PHP work to PHP-FPM.
What Nginx Solves
Nginx helps solve the connection-management side of web serving. It can keep many client connections open without requiring one PHP process per browser connection. It can serve static files directly so PHP does not waste workers on images, CSS, JavaScript, favicons, downloads, or cached public assets. It can proxy requests to PHP-FPM only when a dynamic route actually needs PHP.
That separation matters. A PHP-FPM worker is expensive compared with an idle nginx connection. If a request only needs /assets/app.css, PHP should not be involved. If a request needs public/index.php, nginx forwards it to FPM and waits for a response.
Nginx can also protect upstream services from some kinds of client slowness. Depending on buffering and timeout settings, nginx can absorb slow client reads or writes so the upstream application does not remain tied to the client socket for the entire duration. This is one reason reverse proxies are common in front of dynamic application servers.
Nginx also gives operators a place to enforce limits before PHP runs: maximum body sizes, timeouts, path routing, static file handling, TLS settings, header behavior, access logs, error logs, and sometimes rate or connection controls depending on modules and configuration.
What Nginx Does Not Solve
Nginx does not make slow PHP code fast. If every dynamic request runs a slow database query, calls a slow API, waits on a locked session, or performs expensive CPU work, nginx cannot turn that application into a high-throughput system by itself.
Nginx also does not give PHP-FPM infinite workers. PHP-FPM uses pools of worker processes. Each PHP-FPM child can execute one PHP request at a time in the common process-manager model. If all workers are busy, new FastCGI requests wait or fail depending on queueing, timeouts, and limits. A server may be able to hold many client connections while still having only a small number of PHP requests actively executing.
This distinction is essential. Nginx can handle ten thousand mostly idle client connections more efficiently than a one-thread-per-connection design, but ten thousand simultaneous PHP requests would require enormous application capacity. Database pools, cache servers, session storage, external APIs, CPU, memory, and disk I/O would all become part of the bottleneck.
Do not say "nginx solves scaling" without naming the layer it helps. It helps with client connection handling and reverse-proxy behavior. Application throughput is still an application and infrastructure problem.
PHP-FPM Capacity Planning
When nginx fronts PHP-FPM, capacity planning must include both layers. Nginx has worker processes and worker connection limits. PHP-FPM has pool settings such as maximum children, process manager mode, request timeouts, slow logs, and status pages. The operating system has file descriptor limits, socket backlog behavior, memory, CPU, and network constraints.
A useful mental model is:
client connections -> nginx workers -> FastCGI requests -> PHP-FPM workers -> application dependencies
Each arrow can queue, fail, or slow down. If nginx has enough worker connections but PHP-FPM has too few workers, dynamic requests will wait. If PHP-FPM has many workers but the database allows only a small number of connections, workers may pile up waiting for the database. If the app uses file-based sessions, concurrent requests from the same user may block on session locking.
Start with measurement, not guesswork. Check nginx access logs for request count, status codes, response times if logged, upstream timing if configured, and client behavior. Check nginx error logs for upstream timeouts, connection limits, and FastCGI errors. Check PHP-FPM status and slow logs to see whether workers are busy and where PHP spends time.
Keep-Alive And Slow Clients
HTTP keep-alive lets clients reuse connections for multiple requests. This reduces connection setup overhead, especially with TLS, but it also means idle connections stay open. Nginx is good at handling many such connections, but limits still matter. Worker connections include more than active dynamic work; they include open connections that nginx is managing.
Slow clients are another reason the C10K problem matters. A client on a poor network may read a response slowly. A server design that ties one expensive application worker directly to that client can waste upstream capacity. A reverse proxy can help by decoupling parts of the client-facing connection from the upstream application, but buffering and timeouts must be configured with awareness of the app.
Do not blindly increase keep-alive timeouts or worker connections. Longer timeouts can keep connections around longer. Higher connection limits may require higher file descriptor limits and more memory. The right setting depends on traffic shape, server resources, and upstream capacity.
Static Files And Front Controllers
For PHP applications, nginx should usually serve static files directly from the public document root and pass only dynamic requests to PHP-FPM. This reduces PHP worker usage and keeps the application boundary clean.
A front-controller app commonly sends missing paths to index.php, but that rule must not accidentally execute arbitrary PHP files or expose private directories. The public document root should be the application's public directory, not the repository root. Static assets should live where they are intentionally public. Private configuration, templates, source files, database dumps, and .env files must not be web-accessible.
The C10K discussion is about scalability, but bad routing can become a security problem. Correct document roots and FastCGI parameters matter just as much as connection counts.
Load Testing The Right Layer
Load testing should match the question. If you want to know whether nginx can serve static assets under many keep-alive clients, test static assets. If you want to know whether PHP-FPM can handle dynamic requests, test dynamic routes. If you want to know whether the database is the bottleneck, include realistic database work.
A single benchmark number is rarely enough. Track status codes, latency percentiles, CPU, memory, file descriptors, nginx errors, PHP-FPM worker usage, database connections, cache hit rates, and external API calls. A test that only reports requests per second may hide timeouts, retries, slow tails, or upstream saturation.
For production-like confidence, test the path users actually take. A homepage with cached assets is different from a checkout flow, report export, search endpoint, or upload route. Nginx may handle the edge well while the application bottleneck sits behind it.
Common Mistakes
The first mistake is thinking C10K means ten thousand PHP requests can run at once.
The second mistake is increasing nginx worker connections without checking file descriptor limits, memory, and upstream capacity.
The third mistake is serving static files through PHP instead of nginx.
The fourth mistake is blaming nginx for a PHP-FPM pool that is exhausted or an application dependency that is slow.
The fifth mistake is benchmarking only static files and assuming dynamic pages will behave the same.
The sixth mistake is using copied nginx configuration without testing document roots, FastCGI parameters, timeouts, and logs.
Review Questions
- What is the difference between concurrent connections and requests per second?
- Why is one thread per connection expensive at high concurrency?
- How does nginx's event-driven worker model help with the C10K problem?
- What parts of a PHP deployment does nginx not solve?
- Which logs and status pages help distinguish nginx limits from PHP-FPM limits?
- Why should load tests target the layer they are meant to measure?