Performance Efficiency in Azure: Build for Scale, Not Just Capacity

Introduction

We’re four pillars deep into the Well-Architected Framework series. So far we’ve covered Reliability, Security, and Cost Optimisation. This week we’re looking at Performance Efficiency, arguably the pillar that gets the least attention until something goes wrong.

And something usually does go wrong.

The symptoms are familiar: an application that performs fine in testing but crawls under real load. A VM SKU chosen during a proof of concept that’s been running in production for two years, unchanged. A database that nobody’s looked at since go-live, doing full table scans on a table that now has ten million rows.

Performance problems aren’t always loud. Sometimes they present themselves more slowly over time, response times creeping up week on week, costs quietly inflating, users quietly giving up. By the time it shows up in a support ticket, the problem has been building for months.

The Performance Efficiency pillar is about getting ahead of that. Not over-engineering from day one, but designing with performance in mind and building the feedback loops to catch problems early.

What Is Performance Efficiency?

Microsoft defines Performance Efficiency as the ability of your workloads to scale to meet the demands placed on it efficiently.

The key word is efficiently. This isn’t just about making things fast, it’s about delivering the right performance at the right cost. Throwing bigger VMs at a problem is easy. Designing a system that scales intelligently, baselines what normal looks like, and doesn’t waste money takes thought.

There are four areas I want to focus on here: right-sizing and SKU selection, scaling and autoscale, caching strategies, and monitoring and performance baselines. Together, these cover the vast majority of performance problems I’ve seen in practice. This is not an exhaustive list though.

Right-Sizing and SKU Selection

The most common performance mistake I see isn’t under-provisioning, it’s over-provisioning and then never revisiting it.

A team provisions a D8s_v3 during a migration because thats what the requirements stated. Fair enough. But six months later, that VM is sitting at 15% CPU utilisation with nobody questioning it, because it works and nobody’s been asked to look at it.

Right-sizing isn’t a one-time activity. It’s an ongoing discipline.

Start with the workload profile. Before you select a SKU, understand what the workload actually does. Is it CPU-bound? Memory-bound? Does it have burst characteristics or a steady baseline? A web application with variable traffic has completely different requirements to a database server with consistent query load.

Azure gives you a reasonable set of VM families to work with:

  • D-series (General Purpose), balanced CPU to memory, good starting point for most workloads
  • E-series (Memory Optimised), for memory-intensive workloads like databases and in-memory caching
  • F-series (Compute Optimised), for CPU-heavy workloads with lower memory requirements
  • L-series (Storage Optimised), for high I/O workloads, NoSQL databases, data warehousing

Picking the wrong family matters more than picking the wrong size within a family. A D-series VM is rarely the right answer for a SQL Server workload that needs serious memory headroom, you’ll end up over-speccing on CPU and still hitting memory pressure. Also consider temporary disks in VM SKU’s – you can’t easily transition between a VM without a temp disk to one that has. I always prefer going smaller and scaling up if required, rather than going big and scaling down. It always seems easier to add resources than take them away.

Use Azure Advisor. It’s not perfect, but it will flag obvious right-sizing opportunities using real utilisation data. If Advisor is consistently telling you something is oversized and you’re ignoring it, that’s a cost and performance conversation worth having. Pair it with Azure Monitor metrics over a meaningful period, at least two to four weeks, before making changes to production workloads.

Don’t forget PaaS SKUs. The same logic applies to App Service Plans, Azure SQL tiers, and AKS node pools. A Standard S1 App Service Plan might be perfectly adequate for a low-traffic internal tool, but it’ll become a bottleneck the moment traffic picks up. Equally, provisioning a Business Critical SQL tier for a dev environment is just waste.

The goal is fit-for-purpose, not maximum available.

Scaling and Autoscale

Right-sizing solves the baseline problem. Scaling solves the variable demand problem.

In on-premises environments, scaling was painful, you ordered hardware, waited weeks, racked it, configured it. The result was that teams massively over-provisioned to handle peak load that might only happen a few times a year. The cloud changes this completely. Capacity is elastic. If you’re still provisioning for peak and leaving that capacity running 24/7, you’re paying an on-premises penalty without the on-premises constraints.

Design for scale-out, not scale-up. Scaling up (bigger SKU) is the easy answer but it has limits, you can only go so big, and it usually requires downtime or a restart. Scaling out (more instances) is more resilient, distributes load, and gives you fault tolerance as a side effect. Stateless application tiers lend themselves well to this. If your application has sticky session requirements baked in, that’s a design conversation worth having.

Use autoscale properly. Azure provides autoscale for Virtual Machine Scale Sets, App Service Plans, and AKS node pools. The common failure mode I see is autoscale configured but with thresholds that are either too aggressive or too conservative.

Too aggressive: scale-out triggers at 40% CPU. The system is constantly adding and removing instances, never settling, and you’re paying for the churn.

Too conservative: scale-out triggers at 85% CPU. By the time new instances are provisioned and warmed up, the load spike has already hurt users.

A reasonable starting point for most web workloads is scale-out at 70% CPU sustained for five minutes, scale-in at 30% sustained for ten minutes. The asymmetry matters, scale in slowly to avoid flapping. Adjust based on your actual warm-up time. If your application takes three minutes to initialise, your scale-out trigger needs to account for that lead time.

A good use case for autoscaling is Azure Virtual Desktop. You have the option for shared host pools – few number of large VM’s, or larger number of smaller VM’s. The choice really depends on usage patterns – scaling up to another large VM just for one user isn’t cost effective. Take the time upfront to understand the usage and demand patterns for the service – you may not get it right first time, the key is continuous monitoring and adjusting as needed.

Set sensible minimums and maximums. Autoscale with a minimum of one instance isn’t really autoscale, it’s hoping nothing goes wrong overnight. For anything production-facing, set a minimum that reflects your baseline load requirements. Set a maximum that reflects your cost ceiling and the practical limits of your data tier, there’s no point scaling to twenty app instances if your database connection pool can’t handle the concurrency.

Scheduled scaling is underused. If your workload has predictable patterns, business hours traffic, end-of-month processing, weekly batch jobs, scheduled scaling is a simple win. Pre-warm additional capacity before the load arrives rather than reacting to it.

Caching Strategies

Caching is one of the highest-leverage performance improvements available, yet it’s consistently underused in architectures I review. The principle is simple: if something is expensive to compute or retrieve and the result doesn’t change frequently, cache it. The challenge is identifying the right targets and getting invalidation right.

Good caching candidates include:

  • Reference data that changes infrequently (lookup tables, configuration, product catalogues)
  • Session data shared across multiple application instances
  • Expensive query results that are read far more often than they’re written
  • API responses from third-party services with rate limits or latency

Azure Managed Redis is the go-to for distributed caching in Azure. It’s the successor to Azure Cache for Redis, which will be retired across all tiers by 2028 — so start with Managed Redis from day one. It runs on Redis Enterprise rather than community Redis, offering better performance, zone redundancy by default, and Entra ID authentication baked in rather than bolted on.

In-memory caching within the application process itself works fine for single-instance scenarios, but breaks down as soon as you scale out. Two instances mean two independent caches, and inconsistent behaviour is inevitable. Distributed caching solves this problem.

Cache invalidation is hard but critical. Set TTLs that reflect how stale data can realistically be: a product catalogue may tolerate fifteen minutes, but an account balance cannot. For cached query results, consider which writes should trigger invalidation rather than relying purely on expiry. Avoid caching the same resource at multiple layers — application tier, API gateway, and CDN with different TTLs is a guaranteed source of stale data headaches.

CDN for static content sits adjacent to caching but deserves a mention. Serving JavaScript, CSS, or images through your application tier wastes compute. Instead, put static assets on Azure Blob Storage, front them with Azure CDN or Front Door, and let the edge handle it.

Monitoring and Performance Baselines

You can’t optimise what you can’t measure. And you can’t recognise a problem without knowing what normal looks like.

This is where a lot of teams fall down. Azure Monitor is deployed, Log Analytics workspace is connected, and everyone assumes the job is done. But collecting data and actually using it to understand performance are different things.

Establish baselines before you need them. When you deploy a new workload, spend the first few weeks collecting baseline metrics, CPU, memory, response times, error rates, throughput. Document what normal looks like at different times of day and different points in the business cycle. That baseline becomes your reference point. When something degrades, you want to say “response times are 40% above the two-week average” rather than “something feels slow.”

Focus on the metrics that matter. Azure Monitor surfaces a lot of data. Not all of it is actionable. For most web-facing workloads, I’d focus on:

  • Response time (P95, P99), average response times hide the outliers that users actually experience
  • Error rate, 4xx and 5xx rates trending upward before users start complaining
  • CPU and memory utilisation, with baselines, these become early warning signals
  • Request throughput, sudden drops can indicate upstream issues as quickly as drops in availability
  • Database query performance, slow queries are behind a surprising proportion of application performance problems

Application Insights is the right tool for application-level performance monitoring in Azure. It gives you distributed tracing, dependency tracking, and, crucially, the ability to see slow database queries, slow external API calls, and slow page loads alongside each other. If you’re not using it, you’re flying blind at the application layer.

Set up alert rules that trigger on deviation from baseline, not just on absolute thresholds. An alert that fires when CPU hits 90% is useful. An alert that fires when CPU is 30% above the seven-day average for this time of day is more useful, it catches problems earlier and generates fewer false positives.

Query your logs. Azure Monitor and Log Analytics support KQL (Kusto Query Language), which is genuinely powerful once you get past the initial learning curve. Building a small library of queries that surface slow requests, error spikes, and resource contention gives you an operational toolkit that goes well beyond what dashboards alone can show you.

Applying This in Practice

The temptation with the Well-Architected Framework is to treat each pillar as a checklist, work through it once and consider it done. Performance Efficiency doesn’t work that way. Workloads evolve, traffic patterns change, and the SKU that made sense at launch may not make sense a year later.

What does work is building performance thinking into your regular operations:

  • Review Advisor recommendations monthly. Not every recommendation will be actionable, but the ones that are represent easy wins.
  • Add P95 response time to your team dashboard. If it’s visible, it gets attention.
  • Include performance in your definition of done for new features. A feature that works correctly but doubles database load isn’t finished.
  • Test at scale before you need to. Azure Load Testing gives you the ability to generate realistic load against your application. Use it before a major launch, not after.

Performance efficiency isn’t about having the fastest infrastructure. It’s about building systems that perform predictably under real conditions, scale when they need to, and give you the visibility to catch problems before your users do.

Summary

Area Key Principle Azure Tooling
Right-sizing Fit-for-purpose, review regularly Azure Advisor, Monitor
Scaling Scale-out over scale-up, automate it VMSS, App Service, AKS
Caching Cache what’s expensive, invalidate correctly Azure Managed Redis
Monitoring Baseline first, alert on deviation Azure Monitor, App Insights

Performance efficiency is the pillar that rewards ongoing attention more than any other. Get the foundations right, sensible SKUs, intelligent scaling, strategic caching, and meaningful monitoring, and you’ll spend a lot less time firefighting and a lot more time building.

Further Reading

Your subscription could not be saved. Please try again.
Your subscription to deniscooper.co.uk has been successful.

Join Cloud Engineers Who Build Smarter on Azure

Free checklists, practical guides and the latest posts — built for cloud engineers working with Azure and Terraform.

Leave a Comment