Hasina Razafintsalama

Hasina RAZAFINTSALAMA

← Back to Blog
Audit

10 Points to Audit Before Scaling Your Application

Scaling is not just adding more servers. Before throwing resources at the problem, audit these 10 areas that kill performance in production.

2026-04-28·12 min

Most scaling problems are not infrastructure problems. They are code and architecture problems. Adding servers to a poorly optimised application just makes it fail faster, at a higher bill. This is the checklist to run before you provision anything, because the cheapest capacity is the capacity you were wasting.

How to run this audit

Instrument first. You need real numbers from production, or a production-like load test, not guesses. An APM tool such as Sentry, Datadog or New Relic for request traces, query logging or the database slow query log for the data layer, and a load test with k6 or Locust to reproduce the pressure. Then walk the ten points below against what the data actually shows, roughly in this order, because that is roughly the order of impact.

The ten points at a glance

#PointHow to detectFix
1N+1 queriesQuery count per request in the APM or debug barEager loading
2Missing indexesEXPLAIN ANALYZE on the slowest queriesIndex the columns in WHERE, JOIN, ORDER BY
3Synchronous work in the requestSlow endpoints doing email, PDF, image workMove it to a queue
4Missing cachingNo Cache-Control headers, repeated identical queriesHTTP cache, CDN, query and page cache
5Unbounded queriesEndpoints with no pagination, all() callsAlways paginate and cap the page size
6Session and cache on local diskfile driver in the configMove to Redis before the second server
7Framework caches not enabledconfig, route, view caches missing in prodEnable them in the deploy step
8Logging overheaddebug level in production, disks fillingwarning level, sampling, ship logs off box
9Heavy service resolutionExpensive clients built in every constructorLazy resolve or bind as a singleton
10Memory leaks in workersWorker memory climbing over hoursmax-time or max-jobs restarts, profile over 24h

The rest of this article is the detail behind each row.

1. N+1 queries

The most common cause of a slow list page. You load a collection, then loop over it and touch a relation, and the ORM fires one query per row. A page showing 50 orders with their customer runs 51 queries instead of 2. Detect it by counting queries per request in your APM or debug bar, or turn on strict mode in development so lazy loading throws instead of hiding the problem.

php
// N+1: one query for orders, then one per order for the customer
$orders = Order::latest()->take(50)->get();
foreach ($orders as $order) {
    echo $order->customer->name; // fires a query every iteration
}

// Fixed: two queries total, customers loaded up front
$orders = Order::with('customer')->latest()->take(50)->get();

2. Missing indexes

Run EXPLAIN ANALYZE on your slowest queries and look for a sequential scan on a large table. Index the columns you filter, join and sort on, especially foreign keys, which many frameworks do not index automatically. Use a composite index when you filter on several columns together, in the order the query uses them. Do not index everything: each index slows writes and takes space.

sql
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE tenant_id = 42 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;

-- Seq Scan on a 10M-row table -> add a matching index
CREATE INDEX idx_orders_tenant_status_created
  ON orders (tenant_id, status, created_at DESC);

3. Synchronous work in the request cycle

Anything the user does not need in the response should not run before you send it: sending an email, generating a PDF or a report, resizing an image, calling a slow third-party API, firing a webhook. Push it to a queue, Redis with a worker or SQS, and return immediately. The request handler should do only what is needed to render the answer.

4. Missing HTTP and application caching

  • HTTP layer: Cache-Control and ETag headers on cacheable responses, a CDN in front of static assets and of pages that anonymous users share.
  • Application layer: cache the result of expensive queries and computed values, and full pages for logged-out traffic.
  • Invalidation: decide up front between a short TTL and event-based invalidation. A cache you cannot invalidate correctly is a bug waiting to ship.

5. Unbounded queries and responses

User::all() on a 500k-row table, an API endpoint that returns every record, a report that loads a whole table into memory. Each one works fine in staging and falls over in production. Always paginate, always cap the page size, and use cursor pagination when the offset gets large.

6. Session and cache storage that does not scale

File-based sessions and file cache live on one machine. The moment you add a second app server, users bounce between them and lose their session, and each server has a cold cache. Move sessions and cache to Redis or another shared store before you scale out, not after the incident.

7. Framework caches not enabled in production

bash
# Laravel: run these in the deploy step
php artisan config:cache
php artisan route:cache
php artisan view:cache

The equivalent elsewhere: OPcache enabled with a sane memory limit, and preloading if your stack supports it. These are free wins that many deployments simply forget.

8. Logging and observability overhead

Debug-level logging in production writes millions of lines, fills disks, and adds latency to every request. Set the level to warning or error in production, sample the high-volume events, and ship logs off the box to a system you can actually query. Structured logs, not free text, so you can filter them when something breaks.

9. Heavy work in constructors and service resolution

A service that builds an S3 client, an HTTP client or a report engine in its constructor pays that cost on every request that resolves it, even when the method that needs it is never called. Resolve it lazily, bind it as a singleton so it is built once, or inject a closure that defers construction until first use.

10. Memory leaks in long-running processes

Queue workers, schedulers and daemons keep the process alive across many jobs, so a small leak that is invisible in a request and response cycle compounds over hours until the worker is killed. Profile workers over a full day, not a single job. Set max-time or max-jobs so they restart cleanly on a schedule, and watch resident memory as a metric.

The 80/20 of scaling performance

Most of the win is in the data layer. Fix the N+1 queries, add the missing indexes, and put a cache in front of the expensive work, and you typically get a large throughput improvement with zero extra servers. Infrastructure scaling is what you do after the code and the queries are clean, not instead of it.

Instrument before you optimise. Spend your effort on the bottleneck the data shows, not the one the team assumes exists. The two are different more often than not.

When you actually need more infrastructure

Once the audit is clean and you still hit limits, the usual next steps are: read replicas for read-heavy load, horizontal app scaling now that sessions and cache are shared, a dedicated queue cluster, a connection pooler such as PgBouncer in front of the database, and sharding only as a last resort when a single primary genuinely cannot keep up.

FAQ

What should you check before scaling an application?

The ten points above, in order: N+1 queries, missing indexes, synchronous work in the request, caching gaps, unbounded queries, non-scalable session storage, framework caches, logging overhead, heavy service resolution, and memory leaks in workers. Instrument first so you fix the bottleneck the data points to, not the one you assume.

Does scaling always mean adding servers?

No, and it usually should not be the first move. Most scaling problems are code and architecture problems. Adding servers to an unoptimised application raises the bill and moves the bottleneck without removing it.

How do you find the actual performance bottleneck?

With data, not intuition. An APM tool for request traces, query logging or the slow query log for the database, and a load test to reproduce the pressure. The bottleneck is rarely where the team first looks.

Scaling is an outcome of clean code and a clean data layer, not a substitute for them. Run the audit, fix what the numbers point to, and provision infrastructure only once the application has earned it.

Need help with this topic? Technical Audit

Discover this service