How to set up website server performance that holds under real traffic

Binadit Tech Team 25 September 2026 7 min ler
How to set up website server performance that holds under real traffic

What you will achieve and why it matters

This guide walks through the infrastructure-level changes that have the biggest measurable impact on server response time: process manager tuning, connection handling, database pooling, and caching layers. These are the same changes we apply when a client comes to us with a slow WooCommerce store or a SaaS backend that degrades under concurrent load.

You do not need a full re-architecture to get a faster server. Most performance gains come from a small number of configuration changes applied correctly, in the right order. If you run this as a managed cloud provider europe would for a production client, you will end this guide with a server that handles concurrency predictably instead of falling over at the first traffic spike.

Prerequisites and assumptions

This guide assumes:

  • You are running a Linux server (Ubuntu 22.04 or Debian 12 examples used below) with root or sudo access.
  • Your stack is a typical web application: Nginx as a reverse proxy, PHP-FPM or a similar application runtime, and MySQL or PostgreSQL as the database.
  • You have SSH access and can restart services safely, ideally in a staging environment first.
  • You have basic monitoring already in place, even if it is just htop and server logs. You will need a baseline to compare against.

If you are running a containerized setup (Docker, Kubernetes), the same principles apply, but the commands will differ. We cover container-specific pitfalls separately in our piece on how Docker networking broke checkout under load.

Step-by-step implementation

Step 1: baseline your current performance

Before changing anything, record where you stand. Run a load test against a realistic endpoint:

ab -n 1000 -c 50 https://yourdomain.com/n

Note the requests per second, mean time per request, and the number of failed requests. Also check current server load during the test:

uptimenvmstat 1 5n

Write these numbers down. Every change below should be validated against this baseline.

Step 2: tune PHP-FPM process management

The default PHP-FPM pool configuration is almost never correct for production. Open your pool config:

sudo nano /etc/php/8.3/fpm/pool.d/www.confn

Set the process manager to dynamic and size it based on available RAM, not guesswork. A common rule of thumb: divide available memory by the average memory footprint of a single PHP process (check with ps aux | grep php-fpm to see real usage per worker, typically 30 to 60MB for a WordPress or Laravel app).

pm = dynamicnpm.max_children = 40npm.start_servers = 10npm.min_spare_servers = 5npm.max_spare_servers = 15npm.max_requests = 500n

The pm.max_requests setting matters more than people think. It restarts workers after N requests, which prevents memory leaks in long-running PHP processes from slowly degrading your server.

Step 3: configure Nginx for connection efficiency

Increase worker connections and enable keepalive to reduce the overhead of repeated TCP handshakes:

worker_processes auto;nworker_rlimit_nofile 65535;nnevents {n    worker_connections 4096;n    use epoll;n    multi_accept on;n}nnhttp {n    keepalive_timeout 65;n    keepalive_requests 1000;n    sendfile on;n    tcp_nopush on;n    tcp_nodelay on;n}n

If Nginx sits in front of PHP-FPM over a Unix socket, make sure the socket backlog matches your expected concurrency:

listen = /run/php/php8.3-fpm.socknlisten.backlog = 1024n

Step 4: enable connection pooling for your database

Every unpooled database connection costs a TCP handshake, authentication round trip, and memory allocation on the database side. Under load, this adds up fast. For MySQL, install and configure ProxySQL or a lighter option like mysqlnd_mux at the application layer. For PostgreSQL, PgBouncer is the standard choice:

sudo apt install pgbouncern

Minimal PgBouncer config for transaction-level pooling:

[databases]nyourapp = host=127.0.0.1 port=5432 dbname=yourappnn[pgbouncer]nlisten_port = 6432nauth_type = md5npool_mode = transactionnmax_client_conn = 500ndefault_pool_size = 25n

Point your application's database connection string at port 6432 instead of 5432. This alone typically cuts connection overhead by 60 to 80% on high-concurrency workloads.

Step 5: add a caching layer for repeated reads

Install Redis for object caching and session storage:

sudo apt install redis-servernsudo systemctl enable redis-servern

Set a sane max memory policy so Redis evicts old keys instead of running out of memory:

maxmemory 512mbnmaxmemory-policy allkeys-lrun

For a WordPress or WooCommerce site, connect an object cache plugin (Redis Object Cache) to this instance. For a custom application, wrap your most expensive, most frequently called database queries with a cache-aside pattern:

$cacheKey = "product:{$id}";n$product = $redis->get($cacheKey);nif (!$product) {n    $product = $db->query("SELECT * FROM products WHERE id = ?", [$id]);n    $redis->setex($cacheKey, 300, serialize($product));n}n

Step 6: set correct cache headers at the edge

Static assets should never hit your application server on repeat visits. In Nginx:

location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ {n    expires 30d;n    add_header Cache-Control "public, immutable";n}n

If you run a CDN in front, verify it respects origin cache headers rather than overriding them with a default TTL. We go deeper on this in our guide to CDN and origin caching optimization.

Verification: confirming it actually works

Re-run the same load test from Step 1, at the same concurrency level:

ab -n 1000 -c 50 https://yourdomain.com/n

Compare the following against your baseline:

  • Requests per second: should increase noticeably, often 30 to 100% depending on how unoptimized the starting point was.
  • Mean response time: should drop, and more importantly, the variance between the 50th and 95th percentile should tighten.
  • Failed requests: should be zero at your target concurrency.

Check PHP-FPM worker behavior under load in a second terminal during the test:

watch -n 1 'ps aux | grep php-fpm | wc -l'n

You want to see worker count scale up smoothly and settle, not spike to pm.max_children and stay pinned there. If it pins immediately, your max_children value is too low for the load, or a slow query upstream is holding workers open longer than it should.

Verify PgBouncer or ProxySQL is actually being used, not bypassed:

psql -h 127.0.0.1 -p 6432 -U youruser yourapp -c "SHOW POOLS;"n

Confirm Redis hit rate is meaningful, not near zero:

redis-cli info stats | grep keyspacen

A healthy cache-aside implementation should show a hit ratio above 80% for read-heavy endpoints within a few minutes of traffic.

Common pitfalls to avoid

A few mistakes we see repeatedly when teams apply these changes without full context:

  • Setting max_children too high. More workers than your RAM can support leads to swapping, which is far worse than queuing requests. Calculate from actual memory usage, not a round number.
  • Caching without invalidation logic. A cache that never expires stale data creates subtle bugs that are harder to debug than a slow server. Always set a TTL or an explicit invalidation hook on write.
  • Pooling connections but not adjusting application timeouts. If your app still assumes direct, unpooled connections, transaction-mode pooling can produce confusing errors with session-level features like advisory locks or SET statements.
  • Skipping the baseline. Without a "before" number, you cannot prove any of this worked, and you will not know which change actually mattered if you roll several out at once.

Next steps and related reading

Once server-level tuning is in place, the next layer to examine is how the database itself behaves under sustained load, particularly around index usage and query plans. Our guide on measuring database performance degradation walks through the diagnostic side of that.

If your current setup is a single server and you are starting to hit ceilings that tuning alone cannot fix, the next architectural step is usually horizontal scaling or a move to high availability infrastructure with load balancing across multiple application nodes. That is a bigger change than anything covered here, and worth planning deliberately rather than reactively.

Getting this running without doing it all yourself

Every step above is something your team can implement directly. What tends to be harder is doing it consistently across environments, catching regressions before they hit production, and having someone available when a config change under load does not behave the way the documentation said it would. That is the operational gap a managed cloud provider europe based teams work with is built to close: not replacing your engineers, but backing them with direct, EU-based support instead of a ticket queue.

Need this running in production without building it yourself? See our managed infrastructure services or schedule a call.