The Azure bills that surprise people are not the result of one disaster. They are the result of seven or eight reasonable-seeming decisions, made one at a time, none of which felt expensive when they were made.
By the time the bill arrives, the architecture is in production, the team is busy with the next feature, and "do something about the Azure spend" sits on a backlog that nobody owns. So it doesn't get done. The bill keeps arriving.
The good news is that almost all of this is preventable, but only at the start. Once an architecture is live, almost every cost decision becomes a migration. Here are the seven decisions we now make deliberately on day one of every Azure project.
1. Pick the right App Service tier — and resist scaling up to fix problems
The App Service tier you pick on day one tends to be the tier you live with for the life of the project. People scale up. They rarely scale down.
For a single Blazor Server app serving an internal portal, B2 or S1 is almost always enough. Even a few hundred concurrent users, on Server, will fit. We've shipped systems with [YOU: confirm — I'd put the warehouse system or the manufacturer at "40 concurrent users on a single S1 instance with headroom" — accurate?] without going beyond S1.
The trap is performance problems that look like "we need a bigger plan." Most of them aren't. They're:
- A long-running database query that needs an index.
- A
Virtualizethat wasn't added. - A scoped service caching too much memory per circuit.
- A SignalR connection burning CPU because WebSockets aren't enabled.
Diagnose before scaling. A bigger App Service plan is the most expensive way to fix a missing index.
2. Azure SQL Hyperscale is great — and overkill for most LoB apps
The first time we used Azure SQL we picked the General Purpose serverless tier because the marketing pages made it look like the obvious choice. The bill was small. Everything worked.
We've since seen teams reach for Hyperscale on the assumption that "more scale = better" and pay 4-5× as much for capacity they will never use. Hyperscale is brilliant when you genuinely have a multi-terabyte database. For a line-of-business system with a few hundred gigabytes and predictable working hours, serverless General Purpose with autoscaling is the right answer and a fraction of the cost.
The rule of thumb we use:
- Under 1 TB and predictable load → serverless GP, auto-pause enabled.
- Spiky load with idle nights → serverless GP, the auto-pause savings are real.
- Genuinely multi-TB or globally-distributed → look at Hyperscale or Business Critical.
- "We might need it eventually" → no, you don't.
3. Auto-pause is free money for non-prod
This is the single biggest cost win we make on every project that isn't 24/7 production: auto-pause serverless SQL on dev and staging environments. The database pauses after 60 minutes of no activity and resumes on the next query.
For a dev environment that gets used during UK working hours, that's roughly 60% of the month paused. The math is exactly that simple — you pay for ~40% of the compute hours. And you don't have to remember anything; it just happens.
The cost of waking the database is ~30 seconds on the first request after a pause. Acceptable for dev. Not acceptable for production.
4. Functions are cheap. Functions with the wrong plan are expensive
Azure Functions in the Consumption plan are cheaper than almost anything else in Azure for low-to-medium traffic. The first million executions per month are free. For background jobs, integrations, webhooks and scheduled work, Consumption is the right default.
What is not the right default: putting Functions on a Premium plan because somebody read about cold starts. Cold starts on Consumption Functions are measured in low single-digit seconds, only happen after periods of idle, and do not matter at all for the kinds of work most Functions are doing. Webhook from Stripe? Cold start fine. Nightly batch from cron? Cold start fine. An interactive API the user is waiting on with a stopwatch? OK, then look at Premium — but most workloads aren't that.
We default every Functions app to Consumption, and only move to Premium when we have a specific, named, measured problem that requires it. We have done this exactly twice in five years.
5. Don't use Blob Storage as a database, and don't use the database as a blob store
Blob Storage is cheap if you treat it as blob storage: large objects, written occasionally, read occasionally, with sensible access tiers. It is expensive if you treat it as a key-value database and hit it thousands of times per second. Per-operation pricing exists.
Conversely, storing PDFs, photos and exports as VARBINARY in SQL is one of the most common cost-and-performance traps in line-of-business apps. Your database backup grows linearly with every uploaded file. Restores get slow. Page reads hit blob data unnecessarily.
The rule: structured data in SQL, blobs in Blob Storage, references between them. Pick the right access tier (Hot for in-use, Cool for rarely-touched, Archive for legal-retention-only). For most line-of-business systems we end up with Hot for current-year data and Cool for everything older.
6. Monitoring is not free — and the default settings will surprise you
Application Insights is excellent and we use it on every project. Its default sampling and retention settings will quietly eat money on a busy app.
Three settings worth knowing about on day one:
- Sampling. The default adaptive sampling is fine for many apps but can be too generous on chatty SignalR apps. Tune it down for high-frequency telemetry.
- Retention. Default is 90 days. For most internal LoB apps, 30 is enough.
- Custom metrics. Easy to log thousands of these. Each costs money. Be deliberate about what you emit.
Our normal config: 30-day retention, sampling tuned per app, log analytics workspace shared across the project's environments.
7. Have one bill, with tags, not seven bills
The other big cost-management mistake we've seen — at the level of "how the subscription is organised" rather than "how the architecture is built" — is proliferating subscriptions and resource groups without tagging discipline.
What we do on day one of every project:
- One resource group per environment (dev, staging, prod). Not one per service.
- Every resource gets tags:
project,environment,owner,costcentre. - The Azure Cost Analysis tab is then actually useful — you can filter by tag and see which environment is costing what.
When you have ten resource groups and no tags, every cost investigation starts with archaeology. When you have three resource groups and consistent tags, every cost investigation takes ten seconds.
What we don't bother with
Three things we have stopped doing because they cost more time than they save:
- Reserved instances for projects under £500/month total. The savings are real but not worth the procurement friction at small scale.
- Spot instances for batch work, unless the batch is genuinely tolerant of interruption. Most "batch" work in LoB apps isn't.
- Custom cost-alert dashboards. Azure has a budget feature. Use it. Set an alert at 80% of expected monthly spend and another at 100%. That's enough.
The meta-point
You will get an Azure bill at the end of every month for the entire life of every project you ship. The decisions you make on day one — App Service tier, SQL tier, blob/SQL split, tagging — set the baseline for that bill, forever.
Spending one extra day on those decisions, before any code is written, will pay for itself within the first quarter of running the app. Cost-aware architecture is a day-one activity. By the time the architecture is live, your options collapse to "live with it" or "migrate." Both are more expensive than getting it right the first time.
Red Owl IT is a Microsoft software consultancy in Bath. We design and run Azure architectures for SMEs across the UK — cost-aware by default. If you'd like a second pair of eyes on a bill that's grown faster than the business, we'd happily look at it.