DEV Community

Cover image for Optimizing Nginx for Laravel: Configs That Actually Matter
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Optimizing Nginx for Laravel: Configs That Actually Matter

Nginx sits between your users and your Laravel application, handling every request before PHP sees it. A well-configured Nginx serves static assets without touching PHP, compresses responses to reduce bandwidth, and manages connections efficiently so your PHP-FPM workers handle only the work they need to.

The default Nginx configuration that ships with most Linux distributions is conservative and generic. It is designed to work for any website, which means it is optimized for none. Deploynix provisions Nginx with Laravel-optimized defaults, but understanding what those defaults do — and how to tune them further — gives you the knowledge to squeeze maximum performance from your infrastructure.

This guide covers the Nginx settings that actually impact Laravel application performance, explains why they matter, and provides concrete values you can use. We are skipping the settings that make no measurable difference for PHP applications and focusing on the ones that do.

Worker Configuration: The Foundation

worker_processes

This directive controls how many Nginx worker processes run simultaneously. Each worker can handle thousands of concurrent connections, so the number of workers is not about concurrency — it is about CPU utilization.

worker_processes auto;
Enter fullscreen mode Exit fullscreen mode

auto sets the number of workers to the number of CPU cores, which is the correct choice for almost every situation. A 4-core server gets 4 workers, each pinned to a core for optimal performance. Do not set this higher than your core count — more workers than cores causes context switching overhead that hurts performance.

worker_connections

This sets the maximum number of simultaneous connections each worker can handle.

events {
    worker_connections 1024;
    multi_accept on;
}
Enter fullscreen mode Exit fullscreen mode

The total maximum connections your Nginx can handle is worker_processes * worker_connections. With 4 workers and 1024 connections each, that is 4,096 simultaneous connections. For most Laravel applications, this is more than sufficient.

multi_accept on tells each worker to accept all pending connections at once rather than one at a time. This improves performance under load by reducing the number of system calls.

When to Tune

If you are running a high-traffic site or serving many static assets alongside your Laravel application, increase worker_connections to 2048 or 4096. The memory overhead per connection is minimal (about 8KB), so higher values have negligible cost.

Gzip Compression: The Biggest Quick Win

Enabling gzip compression is the single highest-impact Nginx optimization for most web applications. It reduces the size of HTML, CSS, JavaScript, and JSON responses by 60-80%, which directly improves page load times and reduces bandwidth costs.

gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;

gzip_types
    application/atom+xml
    application/javascript
    application/json
    application/ld+json
    application/manifest+json
    application/rss+xml
    application/vnd.geo+json
    application/vnd.ms-fontobject
    application/x-font-ttf
    application/x-web-app-manifest+json
    application/xhtml+xml
    application/xml
    font/opentype
    image/bmp
    image/svg+xml
    image/x-icon
    text/cache-manifest
    text/css
    text/plain
    text/vcard
    text/vnd.rim.location.xloc
    text/vtt
    text/x-component
    text/x-cross-domain-policy
    text/xml;
Enter fullscreen mode Exit fullscreen mode

Understanding the Settings

gzip_comp_level 5: Compression level ranges from 1 (fastest, least compression) to 9 (slowest, most compression). Level 5 is the sweet spot — it achieves about 90% of maximum compression with roughly 50% of the CPU cost of level 9. Going above 6 provides diminishing returns that are not worth the CPU overhead for dynamic content.

gzip_min_length 256: Do not compress responses smaller than 256 bytes. Very small responses can actually get larger after compression due to the gzip header overhead. The default of 20 bytes is too low.

gzip_proxied any: Compress responses even when the request came through a proxy (like a load balancer or CDN). This is important in multi-server setups where Nginx sits behind a Deploynix load balancer.

gzip_vary on: Adds a Vary: Accept-Encoding header, which tells caches and CDNs to store both compressed and uncompressed versions. This prevents a cache from serving a compressed response to a client that does not support gzip.

What Not to Compress

Images (JPEG, PNG, WebP, GIF) and already-compressed files (ZIP, PDF) should not be gzip-compressed. They are already compressed, and attempting to compress them wastes CPU with no size reduction. Notice that image/jpeg and image/png are not in the gzip_types list above.

Static File Caching: Stop Hitting PHP for CSS

Every request that Nginx can serve without involving PHP is a request that leaves your PHP-FPM workers available for actual application logic. Static assets — CSS, JavaScript, images, fonts — should be served directly by Nginx with long cache headers.

location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
    try_files $uri =404;
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Settings

expires 1y: Sets both the Expires header and the Cache-Control: max-age header to one year. This tells browsers to cache these files locally and not re-request them.

Cache-Control "public, immutable": public allows CDNs and proxies to cache the file. immutable tells browsers that the file will not change during its cache lifetime, preventing conditional requests (If-Modified-Since) that still generate network traffic.

access_log off: Static file requests do not need to be logged. Disabling access logging for these requests reduces disk I/O and keeps your log files focused on actual application requests.

But What About Cache Busting?

If you are using Laravel Mix or Vite (which you should be), your asset URLs include a content hash that changes whenever the file changes. For example, app.a1b2c3d4.css becomes app.e5f6g7h8.css when the CSS changes. This means you can safely cache assets for a year — when the content changes, the URL changes, and the browser fetches the new file.

Keepalive Connections: Reducing TCP Overhead

Every HTTP request requires a TCP connection. Without keepalive, each request opens a new connection, completes the request, and closes the connection. The TCP handshake adds latency, and connection establishment consumes server resources.

Client Keepalive

keepalive_timeout 65;
keepalive_requests 100;
Enter fullscreen mode Exit fullscreen mode

keepalive_timeout 65: Keep idle connections open for 65 seconds. This allows a browser to reuse the same connection for multiple requests (loading a page, then its CSS, JavaScript, and images).

keepalive_requests 100: Allow up to 100 requests on a single keepalive connection before closing it. This prevents any single connection from monopolizing resources indefinitely.

Upstream Keepalive (to PHP-FPM)

If you are using PHP-FPM over TCP (rather than a Unix socket), keepalive connections to the upstream reduce connection overhead:

upstream php-fpm {
    server unix:/var/run/php/php-fpm.sock;
    keepalive 16;
}
Enter fullscreen mode Exit fullscreen mode

The keepalive 16 directive maintains a pool of 16 idle connections to the PHP-FPM socket. This eliminates the overhead of establishing a new connection for each request. The value should be lower than your PHP-FPM worker count to avoid tying up workers with idle connections.

Buffer Sizes: Preventing Disk Spills

Nginx uses buffers to hold request bodies and upstream responses in memory. If a buffer is too small, Nginx writes temporary data to disk, which is dramatically slower.

client_body_buffer_size 16k;
client_max_body_size 64m;

fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 32k;
Enter fullscreen mode Exit fullscreen mode

Understanding the Settings

client_body_buffer_size 16k: Buffer for the request body (POST data). Most form submissions fit in 16KB. If the body exceeds this size, it is written to a temporary file on disk.

client_max_body_size 64m: Maximum allowed request body size. Set this to accommodate your largest expected upload. A file upload that exceeds this limit gets a 413 (Request Entity Too Large) error. Adjust based on your application's needs — if you allow 50MB file uploads, set this to at least 64MB.

fastcgi_buffer_size 32k: Buffer for the first part of the FastCGI response (typically the response headers). 32KB is sufficient for Laravel's response headers.

fastcgi_buffers 16 16k: Allocates 16 buffers of 16KB each (256KB total) for the FastCGI response body. This is enough for most Laravel responses. If your pages are larger than 256KB, increase either the number or size of buffers.

fastcgi_busy_buffers_size 32k: The amount of buffer that can be sent to the client while still reading from FastCGI. This enables Nginx to start sending the response before it has finished reading the entire upstream response.

Security Headers: Not Performance, But Essential

While we are configuring Nginx, these security headers should be on every production Laravel application:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Enter fullscreen mode Exit fullscreen mode

These add negligible overhead (a few bytes per response) and protect against common web security issues.

The PHP-FPM Connection: FastCGI Tuning

The connection between Nginx and PHP-FPM is where most optimization opportunities lie for Laravel applications.

location ~ \.php$ {
    fastcgi_pass unix:/var/run/php/php-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;

    fastcgi_read_timeout 60s;
    fastcgi_send_timeout 60s;
    fastcgi_connect_timeout 5s;
}
Enter fullscreen mode Exit fullscreen mode

fastcgi_read_timeout 60s: How long Nginx waits for PHP-FPM to send the response. The default of 60 seconds is appropriate for most applications. If you have long-running requests (report generation, data exports), you may need to increase this. But a better solution is to move long-running operations to queue workers.

fastcgi_connect_timeout 5s: How long Nginx waits to establish a connection to PHP-FPM. If this timeout fires, PHP-FPM is probably overloaded or crashed. A short timeout here ensures failed connections are detected quickly.

Unix socket vs TCP: Always use a Unix socket (unix:/var/run/php/php-fpm.sock) when Nginx and PHP-FPM are on the same server. Unix sockets eliminate TCP overhead and are roughly 5-10% faster for local connections.

Rate Limiting: Protecting Your Application

Nginx can rate-limit requests before they reach PHP, protecting your application from abuse and reducing the load on PHP-FPM during traffic spikes or attacks:

limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

location /api/ {
    limit_req zone=api burst=60 nodelay;
    # ... rest of location config
}
Enter fullscreen mode Exit fullscreen mode

This limits each IP address to 30 requests per second to your API endpoints, with a burst allowance of 60 requests. Requests exceeding the limit receive a 429 (Too Many Requests) response without ever touching PHP.

What Deploynix Sets by Default

Deploynix provisions Nginx with sensible defaults for Laravel applications, including:

  • Worker processes set to auto
  • Gzip compression enabled with appropriate types and compression level
  • Static file caching with long expiry headers
  • Appropriate buffer sizes
  • Security headers
  • Unix socket connection to PHP-FPM
  • Keepalive connections enabled

These defaults work well for the majority of applications without any manual tuning. The optimizations described in this guide are for teams that want to squeeze additional performance from their Nginx configuration or have specific requirements (very large uploads, high static asset volume, aggressive rate limiting) that differ from the defaults.

Testing Your Configuration

After making changes, always validate your Nginx configuration before reloading:

sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

This checks for syntax errors without affecting the running server. Only reload Nginx if the test passes:

sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Use reload rather than restart — reload applies the new configuration without dropping existing connections, while restart terminates all connections.

Conclusion

Nginx optimization for Laravel applications comes down to a handful of settings that make a real difference: gzip compression reduces response sizes by 60-80%, static file caching eliminates unnecessary PHP processing, proper buffer sizes prevent disk spills, and keepalive connections reduce TCP overhead.

The most important insight is that the biggest performance gains come from what Nginx does not send to PHP. Every static file served directly by Nginx, every compressed response that saves bandwidth, and every rate-limited abusive request stopped at the Nginx layer is work that your PHP-FPM workers never have to do.

Deploynix provisions Nginx with optimized defaults so you start with a good configuration. From there, monitor your application's behavior, identify specific bottlenecks, and apply targeted tuning. The settings in this guide are the ones that matter — everything else is noise.

Top comments (0)