How to Scale an Application Without Sacrificing Performance or Reliability

An ecommerce application prepares for its largest promotion of the year by increasing the maximum number of application servers from 20 to 80.

Traffic arrives, autoscaling starts and dozens of new instances appear exactly as planned.

Yet checkout gets slower.

Every new instance opens its own pool of database connections. The database reaches its connection limit, requests begin waiting, some time out, and clients retry. Those retries create even more work. Within minutes, the company has four times as many application servers but a less reliable checkout experience.

The system scaled.

The application did not scale well.

This is the distinction that matters when applications grow. Scaling is not simply the ability to add resources. It is the ability to increase useful capacity while keeping important workflows within acceptable performance and reliability limits.

Microsoft's Well-Architected guidance makes the same point: adding replicas at the infrastructure layer does not guarantee that an application can use them effectively. Scaling has to account for application state, data stores, dependencies, initialization time and bottlenecks across the entire workload.

For Levelworks, that sits at the intersection of three App Health aspects: Scalable, Performant and Resilient. Growth should increase the amount of work the application can handle without turning higher demand into excessive latency, cascading failures or unpredictable behavior.

Scale the bottleneck, not whatever is easiest to duplicate

Application servers are often among the easiest resources to scale horizontally.

That can make them the first thing teams increase when traffic rises.

But if the application tier is not the constraint, more instances may accomplish very little.

Imagine a system where:

  • application instances are at 45% CPU;
  • database connections are at 95%;
  • a payment API is close to its request limit;
  • a reporting queue is steadily growing.

Adding more application instances may increase pressure on the database and payment provider without removing the actual constraint.

Microsoft specifically warns that scaling upstream components can unintentionally overload downstream dependencies and notes that stateful components such as databases are common scaling bottlenecks.

Before scaling, identify what currently limits useful throughput.

That might be:

  • compute;
  • database capacity;
  • connections;
  • storage throughput;
  • queue-processing rate;
  • network capacity;
  • an external API.

Then make the scaling decision around that constraint.

[Internal Link: “How to Know if Your Application Is Ready to Scale”]

Prefer horizontal scaling where the workload supports it

There are two broad ways to increase resource capacity.

Vertical scaling gives an existing resource more CPU, memory or another capability.

Horizontal scaling adds more instances that can share the workload.

Vertical scaling can be useful, particularly for components that cannot easily be distributed. Its limitation is that individual machines and managed services eventually have maximum sizes, while resizing may also require disruption depending on the technology.

Horizontal scaling generally provides more flexibility for application services because capacity can be added incrementally as demand changes.

Kubernetes' Horizontal Pod Autoscaler, for example, changes the number of running workload replicas according to observed metrics, adding Pods when additional capacity is required and reducing them when demand falls.

However, horizontal scaling only works if the application is designed so additional instances can genuinely share the work.

Adding replicas to a component that depends heavily on one specific server does not create the expected elasticity.

Remove unnecessary instance affinity

A common obstacle to horizontal scaling is locally stored state.

Suppose Server A stores a user's session in memory.

The next request reaches Server B.

Server B does not have that session information, so the application either fails or requires the load balancer to keep sending the same user back to Server A.

That technique—often called sticky sessions or session affinity—can work, but it reduces flexibility.

AWS recommends making application services stateless where practical, or moving required session state outside individual application instances. When requests do not depend on locally stored state, any healthy instance can handle them, making capacity easier to add and remove.

This does not mean the entire application must contain no state. Most useful applications have enormous amounts of state.

The important distinction is where that state lives.

Application instances become much easier to scale when persistent or session state is stored in systems intended to be shared across them rather than tied to one process.

Scale components independently when demand affects them differently

Not every part of an application grows at the same rate.

Imagine a SaaS application containing:

  • public APIs;
  • reporting workers;
  • document processors;
  • notification workers;
  • scheduled import jobs.

A new enterprise customer might dramatically increase document processing without materially increasing interactive API traffic.

Scaling the whole application uniformly would waste resources and could create unnecessary pressure on shared dependencies.

Instead, components that experience different demand patterns should be independently scalable where the architecture permits it.

Microsoft's scaling guidance recommends defining appropriate scale units: components or groups of components that should grow together according to how the workload uses capacity.

For example:

Demand change

Component that may need scaling

More web/API traffic

Application instances

More uploaded documents

Processing workers

More asynchronous jobs

Queue consumers

Higher read volume

Read data layer

More stored data

Database/storage architecture

This keeps capacity closer to actual demand rather than treating the entire application as one indivisible block.

Do not let compute scaling overwhelm the database

Application compute is relatively easy to multiply.

Databases are frequently the point where that strategy becomes harder.

Suppose each application instance has a connection pool of 30.

At 10 instances, the application may open up to 300 database connections.

At 50 instances, that theoretical maximum becomes 1,500.

If the database can support only a fraction of that load efficiently, autoscaling the application tier can create the database bottleneck that damages performance.

Microsoft's current scalability guidance identifies stateful dependencies, particularly databases, as components that require careful treatment as other workload layers expand.

Watch not only database CPU but:

  • connection utilization;
  • query latency;
  • lock contention;
  • I/O;
  • write throughput;
  • replication delay where relevant.

Depending on the workload, growth may eventually require techniques such as read replicas, partitioning or sharding. Those are significant architectural choices, not automatic next steps.

The broader principle is simpler:

Every time you increase upstream capacity, verify that the next dependency can accept the additional work.

Scale background work independently from user requests

Asynchronous processing is often useful for scalability because not every task needs to complete during the user's request.

A user might upload a document and receive confirmation while separate workers later perform processing, indexing or notification.

Queues allow the application to absorb short bursts without requiring all downstream work to finish immediately.

But queues do not remove capacity requirements.

If jobs arrive at 20,000 per minute while workers can complete only 12,000, the application is accumulating debt every minute.

The interactive experience may remain healthy initially while background completion times deteriorate.

Monitor:

  • job arrival rate;
  • processing throughput;
  • queue depth;
  • age of the oldest job;
  • retry volume.

Scale workers according to the workload they actually process rather than simply scaling them whenever the web tier grows.

This protects performance while preventing delayed work from becoming a hidden reliability problem.

Use autoscaling signals that represent real pressure

CPU utilization is a convenient scaling signal.

It is not always the right one.

An API might become overloaded because all request workers are waiting for database connections while CPU remains relatively low.

A queue processor may need more replicas because backlog is growing, not because its current workers have crossed a CPU threshold.

Kubernetes supports autoscaling from resource metrics such as CPU and memory as well as custom and external metrics, allowing scaling rules to reflect the workload rather than being tied to one generic signal.

Depending on the component, useful signals might include:

  • requests per instance;
  • active request count;
  • queue depth;
  • message age;
  • concurrent jobs;
  • database utilization.

Choose the signal that tells you capacity is becoming insufficient, not merely one that happens to be easy to collect.

Scale early enough to absorb the traffic

Autoscaling has latency.

A new instance may need to:

  1. start;
  2. initialize the runtime;
  3. load configuration;
  4. establish connections;
  5. warm caches;
  6. pass health checks;
  7. enter load balancing.

If that takes two minutes but demand can double in thirty seconds, reactive scaling may always be late.

Microsoft recommends considering the initialization time of scale units and maintaining sufficient buffer capacity to absorb load while new resources become ready. It also notes that some managed services take substantially longer to scale than ordinary compute instances.

For predictable peaks, scale ahead of the event where appropriate.

For unpredictable demand, maintain enough operational headroom that the existing fleet can tolerate the increase during the scaling delay.

Running every component permanently at the edge of saturation may minimize unused infrastructure, but it gives the system very little room to respond when conditions change.

Protect the system when scaling cannot keep up

No scaling strategy provides infinite capacity.

Eventually demand can rise faster than resources can be added, or a constrained downstream system can reach its practical maximum.

At that point, reliability depends on how the application handles overload.

Google SRE recommends that overloaded services provide reasonable but reduced results where possible and use techniques such as controlled load shedding when demand exceeds available capacity. Its production guidance also stresses testing systems beyond rated capacity to understand how they behave under overload.

The objective is to stop overload from turning into uncontrolled collapse.

Depending on the application, that may mean:

  • temporarily disabling expensive optional features;
  • limiting lower-priority workloads;
  • rejecting excess requests quickly;
  • controlling queue growth;
  • applying rate limits.

This is different from making the application artificially unavailable.

It is acknowledging that once capacity is exhausted, doing slightly less useful work can be safer than attempting everything and failing across the whole system.

Make retries capacity-aware

Retries deserve special attention as an application scales because they can transform a small overload into a larger one.

Suppose a service receives 10,000 requests per second and 10% begin timing out.

If every failed request is retried immediately, the backend receives additional traffic precisely when it is already struggling.

Those retries can themselves fail and retry again.

AWS recommends controlling retries with a maximum attempt count, exponential backoff and jitter. Its reliability guidance specifically warns that retries during resource overload can worsen the problem and that retry mechanisms at multiple application layers can multiply traffic into a retry storm.

Google SRE describes the same positive-feedback pattern as a contributor to cascading failures: overload causes failures, failures increase retries, retries increase load and remaining healthy capacity becomes even more constrained.

Scaling safely therefore requires controlling amplified demand, not only genuine customer traffic.

Preserve redundancy as the system scales

Performance efficiency sometimes pushes teams toward high utilization.

Reliability needs some unused capacity.

If every application instance is operating at its practical maximum during peak traffic, losing one instance immediately transfers its work to other instances that have no available headroom.

The remaining nodes slow down or fail, transferring even more traffic to whichever nodes survive.

Microsoft's Well-Architected trade-off guidance warns that scaling too close to demand can leave a workload vulnerable to sudden usage spikes or node failure.

This means scaling targets should account for failures as well as normal demand.

If losing one instance, availability zone or another expected component would immediately overwhelm the remaining capacity, the system may be efficient on paper while being fragile in production.

Scale-in needs as much care as scale-out

Most scaling discussions focus on adding resources.

Removing them can also affect reliability.

Imagine a background worker processing a 20-minute job.

Autoscaling sees lower demand and terminates the instance after 15 minutes.

Unless the workload can resume or safely return the task to the queue, work may be lost.

The same problem can occur with:

  • active connections;
  • in-memory sessions;
  • long-running requests;
  • partially completed tasks.

Microsoft recommends designing long-running operations so instances can drain or work can be broken into smaller units that tolerate scale-in safely.

Before reducing capacity, determine whether the resource is actually idle or simply occupied by work that metrics do not represent clearly.

Scaling should be reversible without corrupting or silently abandoning business activity.

Verify scaling with realistic testing

A scaling architecture is theoretical until it has been exercised.

Do not test only whether more instances appear.

Observe what happens throughout the workload as demand increases.

Measure:

  • throughput;
  • p95 and p99 latency;
  • error and timeout rates;
  • resource utilization;
  • database connections;
  • queue backlog;
  • dependency latency;
  • scale-out time.

Then continue far enough to discover where performance begins deteriorating.

A useful test might reveal:

Traffic level

App instances

p95 latency

Main constraint

Normal

8

450 ms

None

2×

14

520 ms

None

3×

22

710 ms

DB connections rising

4×

31

1.9 s

Database

5×

38

4.8 s

Database saturated

The lesson is not that 38 instances are insufficient.

It is that application scaling stops producing useful capacity once the database reaches its limit.

[Internal Link: “Load Testing vs Stress Testing: What Does Your App Need?”]

Watch user-facing performance while scaling occurs

Scaling is successful only if customers continue receiving acceptable service.

Infrastructure metrics can suggest that everything is working because CPU falls after new instances arrive.

Meanwhile, p95 latency might have doubled because cache misses increased or a downstream dependency is overloaded.

Track the scaling event alongside:

  • business workflow latency;
  • error rate;
  • timeout rate;
  • queue delay;
  • downstream latency.

Microsoft's Performance Efficiency guidance defines successful scaling in terms of adapting to changing demand without negatively affecting user experience, rather than simply increasing resource counts.

For Levelworks, this is where Scalable and Performant App Health need to be considered together.

More capacity is useful only if it protects the experience the capacity was added to serve.

Know the difference between a scaling problem and an efficiency problem

Some systems do not need more resources yet.

They need to use existing resources better.

If one application instance can handle 1,000 requests per second except for one endpoint executing an inefficient query, adding twice as many instances may postpone the problem without resolving it.

Similarly, if large payloads, unnecessary network calls or expensive synchronous operations dominate response time, infrastructure growth can become an expensive substitute for optimization.

Before expanding capacity, ask:

Would more resources increase useful throughput proportionately?

If the answer is no, optimization should probably precede scaling.

This protects both performance and cost while ensuring the eventual scaling mechanism has a healthier workload to distribute.

Scale one layer, then verify the next

A practical approach to scaling without creating new reliability problems is:

  1. Identify the constrained component.
  2. Determine whether optimization can remove the constraint first.
  3. Choose vertical or horizontal scaling appropriate to that component.
  4. Check how increased capacity affects downstream systems.
  5. Verify that application state does not prevent distribution.
  6. Choose scaling signals that reflect real demand.
  7. Keep enough headroom for scaling delay and component failure.
  8. Control retries and overload behavior.
  9. Test performance across increasing load.
  10. Observe production behavior and adjust thresholds.

Scaling is therefore less about "adding servers" than managing the relationships between components as workload grows.

[Internal Link: “Application Capacity Planning: Preparing Your App for Growth”]

Good scaling preserves the qualities users already depend on

Growth puts an application under a different kind of pressure.

A design that works well with ten servers may behave very differently with fifty. A database that comfortably supports today's request volume may become the limiting factor once application compute expands. Retries that seem harmless under normal traffic can amplify overload, while aggressive scale-in can remove resources that still hold active work.

That is why Levelworks treats Scalable, Performant and Resilient App Health as related but distinct qualities.

A scalable application can increase useful capacity.

A performant application continues meeting acceptable response times while doing so.

A resilient application avoids turning overload, component failure or changing capacity into a wider outage.

The objective is therefore not to scale the largest number of servers or process the highest theoretical request rate.

It is to make sure that as demand grows, capacity increases without shifting the bottleneck somewhere more dangerous, response times remain acceptable, and individual constraints do not turn into system-wide failures.

When those three conditions hold, the application is not merely getting bigger.

It is scaling well.

‍

Continue Reading