Lizrd is in private beta. Request early access →

Moving SaaS to the Cloud: Don’t Carry Your Data Center Sizing Assumptions With You

Georg Hennerbichler, CEO Lizrd · · 18 min read

Migrating a SaaS product from a local data center into AWS is usually treated as an architecture project. Teams spend months thinking about service boundaries, databases, networking, identity, observability, availability zones, backups, disaster recovery, migration sequencing, and how customers should move from the old platform to the new one.

All of that matters, but one question often receives surprisingly little attention: how big does the new platform actually need to be? That sounds like a simple capacity-planning exercise, yet sizing decisions made during a cloud migration can define the economics of the platform for years. If the default approach is “let’s provision enough so we definitely don’t run into trouble,” an organization can arrive in AWS with a technically modern architecture and an unnecessarily expensive operating model.

The irony is that elasticity is one of the main reasons to move to the cloud in the first place. Many migrations still carry data-center sizing assumptions into an environment where capacity can often be changed in minutes.

Data-center sizing and cloud sizing solve different problems

In a traditional data center, over-provisioning is often rational. If you buy a physical server today and expect it to run for five years, you need to think about future growth because adding capacity later may involve procurement cycles, new hardware, rack space, networking changes, contracts, and maintenance windows. The natural question therefore becomes: how much capacity might we need over the lifetime of this hardware?

Cloud infrastructure changes that calculation. The better question becomes: how much capacity do we need now, what headroom do we need for reliability, and how quickly can we safely add more?

Suppose an application currently requires around 12 vCPUs under peak load. In a data center, buying a 32-core machine may be entirely reasonable because you are planning for several years of growth. In AWS, for example, provisioning the equivalent of 32 vCPUs continuously because you might need that amount in two years means paying for two years of future capacity today. If the workload is stateless and can scale horizontally, that extra capacity may offer very little actual protection.

This distinction is one of the most important mindset changes in a cloud migration. Capacity no longer has to be purchased primarily around future uncertainty; it can increasingly be adjusted around observed demand.

“Let’s just be safe” needs a price tag

One of the most common phrases during infrastructure design is “let’s just be safe.” There is nothing wrong with safety margins, because production systems need headroom, redundancy, and room to absorb failures. The problem starts when “safe” has no definition.

A migration team may decide to run 16 application instances instead of 8, choose a database with 32 vCPUs instead of 16, deploy three large Kubernetes nodes per availability zone, give staging almost the same capacity as production, and retain every log for twelve months. Every decision can be defended individually, yet together they can make the platform dramatically more expensive than necessary.

Whenever additional capacity is proposed, I like asking a very simple question: what specific risk are we buying down with this extra infrastructure? If the answer is concrete, the extra capacity may be justified. Perhaps the workload needs to survive the loss of an availability zone without scaling. Perhaps a database resize takes too long to be part of an incident response. Perhaps there is a contractual latency target that requires substantial headroom.

If the answer is essentially “just in case,” the decision deserves another look.

Measure the workload, not the server

One of the easiest mistakes during migration is selecting cloud infrastructure based on the specifications of existing hardware. Imagine the current database server looks like this:

  • 32 vCPUs
  • 256 GB RAM
  • 4 TB SSD

The obvious migration path is to choose a managed database instance with roughly equivalent specifications. That feels safe because nothing appears to be getting smaller.

Now look at ninety days of actual usage:

  • CPU average: 18%
  • CPU p95: 43%
  • CPU peak: 71%
  • Memory average: 82 GB
  • Memory peak: 126 GB
  • Database size: 1.4 TB
  • Storage growth: 35 GB/month

That tells a very different story. The existing server may have been oversized for historical reasons. Perhaps it was purchased five years ago with growth expectations that never materialized. Perhaps other workloads used to run on it. Perhaps a performance problem was once solved by throwing hardware at it and nobody revisited the configuration afterwards.

Migrating the hardware specification rather than the workload simply transfers years of accumulated assumptions into the new cloud platform. Before sizing the target environment, I would therefore collect real workload data for every meaningful component. At minimum, that means CPU distribution, memory distribution, storage growth, IOPS, throughput, network usage, request rates, concurrency, database connections, queue depth, batch windows, and daily or weekly traffic patterns.

Thirty days can be useful, ninety days is much better, and for seasonal businesses you may need a full year of history to understand what “normal” really means.

Don’t size permanent capacity for temporary peaks

Peak load matters, especially for SaaS platforms with strict availability requirements, but peak capacity and permanent capacity are not the same thing.

Suppose normal traffic requires eight application instances, while a monthly processing event briefly requires twenty. Running twenty instances continuously is one solution, but in a cloud-native environment it is rarely the only one. Depending on the workload, you may be able to use horizontal autoscaling, scheduled scaling, queue-based scaling, burst capacity, temporary worker pools, serverless processing, separate batch compute, or a reserved baseline combined with on-demand capacity.

The key question is not “what is the largest amount of capacity we have ever needed?” It is “what capacity must always exist, and what can be created when demand requires it?” A workload that needs twenty instances for two hours per month should not automatically pay for twenty instances for roughly 730 hours every month.

Separate one-way doors from two-way doors

One of the most useful decision-making frameworks during a migration is to distinguish one-way doors from two-way doors.

Some architectural decisions are difficult, costly, or disruptive to reverse. Choosing a primary cloud provider is relatively close to a one-way door. So is committing deeply to a database technology once large amounts of data and application logic depend on it. Region selection may also become difficult to reverse because of latency, compliance, data residency, or customer contracts. Those decisions deserve serious analysis.

Other decisions are much easier to change. Choosing whether a stateless service runs on an instance with 4 vCPUs or 8 is usually a two-way door. Changing a Kubernetes CPU request from 2 cores to 1.5 is reversible. Increasing an autoscaling ceiling from 20 replicas to 40 is reversible. Changing the instance type of a stateless worker can often be done with minimal disruption.

Teams often spend their caution budget in the wrong places by massively over-provisioning easily reversible infrastructure because choosing too little feels dangerous, even when the decision can be corrected quickly. For two-way-door decisions, I prefer a different approach: start with evidence, add reasonable headroom, monitor aggressively, and adjust when the data tells you to change.

The ability to change capacity should be treated as part of the architecture rather than as a failure of the original sizing exercise.

Build headroom deliberately

The opposite extreme is also dangerous. Cloud infrastructure being elastic does not mean production should run permanently at 95% utilization. The goal is not maximum utilization; the goal is intentional headroom.

A latency-sensitive API may need significantly more spare capacity than an asynchronous worker. A stateful database that takes twenty minutes to resize may need more headroom than a stateless Kubernetes deployment that can add replicas in thirty seconds.

For example, a workload with CPU p95 around 62% and peaks around 78% may be perfectly healthy. Another workload with CPU p95 around 18% and peaks around 27% may also have a valid reason for that level of spare capacity, but the reason should be explicit.

Headroom should reflect the characteristics of the workload: scaling speed, failure modes, latency requirements, traffic burstiness, and recovery expectations. The important point is that headroom should be a conscious engineering decision rather than an inherited configuration.

Production, staging, and development should not automatically have the same economics

A migration often introduces a cleaner and more standardized environment model, which is good. The problem is that standardization can accidentally lead to capacity parity.

Production might run twelve application replicas, a highly available database, a multi-node cache, and three Kubernetes node pools. During migration, it is tempting to reproduce a similar topology in staging because it simplifies configuration and reduces differences between environments.

That can be useful for some workloads, but it can also become expensive quickly. Imagine staging receives 4% of production traffic but runs 50% of the infrastructure. Development environments may be even more lightly used while remaining available around the clock.

The right question is not whether staging should look like production. It is which characteristics of production actually need to be reproduced. You may need the same database engine without needing the same database size. You may need the same Kubernetes manifests without needing the same replica counts. You may need the same availability-zone architecture for disaster-recovery testing without needing it every day.

For every non-production environment, define what the environment is for and size it around that purpose.

Databases deserve extra caution

Application servers are relatively forgiving because stateless capacity can often be added and removed quickly. Databases are different. Database sizing decisions interact with memory, storage, IOPS, connection limits, backups, replicas, failover, and application behavior, which makes downsizing more sensitive and usually increases the perceived blast radius.

That makes databases one of the most common places where “let’s just be safe” becomes expensive. The practical approach is to distinguish the capacity required for current workload behavior from capacity added as a safety margin.

Suppose an existing database uses 8 vCPUs at p95, peaks at 11, and normally consumes 60 GB of memory. Moving directly to a 32-vCPU cloud database because that resembles the existing data-center server is probably excessive. Starting closer to 16 vCPUs with sufficient memory may be entirely reasonable if you have good monitoring, understand scaling limitations, and know how quickly you can move to a larger tier.

Storage should be treated similarly. If the database contains 1.4 TB and grows by 35 GB per month, provisioning around measured growth is much better than blindly carrying a 4 TB storage allocation into the cloud.

Kubernetes can hide over-provisioning surprisingly well

Kubernetes introduces another layer where conservative sizing can quietly accumulate.

Suppose a service is configured with 10 replicas, 2 CPU requested per pod, and 4 GiB memory requested per pod. The scheduler now treats that workload as requiring 20 CPUs and 40 GiB of memory even if actual utilization is much lower.

If p95 usage is closer to 700m CPU and 1.8 GiB memory, the node fleet may be substantially larger than the workload actually requires.

This becomes particularly important during migration because teams often convert existing application sizing assumptions directly into Kubernetes requests and limits. A service that historically ran on a large virtual machine may receive generous pod requests because nobody wants the migration to fail due to resource pressure.

That is understandable during the initial move. The dangerous part is never revisiting those settings. A sensible post-migration process should explicitly compare requests with actual p50, p95, and peak usage and gradually tighten resources where the evidence supports it.

Autoscaling does not automatically make infrastructure efficient

Autoscaling can create the impression that the platform is already optimized because capacity changes dynamically. That is not necessarily true.

Consider a service configured with a minimum of eight replicas, a maximum of forty, and a target CPU utilization of 60%. If application improvements later reduce CPU consumption by 45%, normal traffic may require only four replicas, but the autoscaler will never reach four because the minimum remains eight.

The application became substantially more efficient, yet infrastructure cost barely changed. The same pattern appears with Kubernetes node-pool minimums, database tiers, queue-worker counts, scheduled scaling rules, and reserved capacity.

Whenever the application changes materially, scaling assumptions deserve to be revisited too.

Be careful with commitments during migration

Reserved Instances, Savings Plans, committed-use discounts, and similar mechanisms can create significant savings, but migration is usually not the moment to optimize commitments aggressively.

During the first few months, workload shapes are still changing. Services may move between instance families, databases may be resized, containers may consolidate, and autoscaling behavior may change once real production traffic reaches the new environment.

Committing too early can lock in the wrong baseline. I generally prefer to stabilize the architecture first, observe real production behavior, and then commit against the portion of usage that is genuinely predictable.

If a platform runs 100 units of compute today, I would much rather commit against 50 or 60 units that I am confident will remain than optimize the theoretical discount by committing to 95 and discovering three months later that the workload only needs 65.

Commitments should follow understanding, not precede it.

Observability can become a hidden migration cost

Another cost category that frequently surprises teams is observability.

Cloud migrations usually improve logging, metrics, tracing, and alerting, which is a good thing. The migration is also a moment when teams tend to increase telemetry because they want excellent visibility into the new platform.

The problem appears when everything is collected at maximum detail forever. Imagine a platform generating 8 TB of logs per month after migration. A large part of that volume might come from debug logs, health checks, repeated request metadata, or services that do not require long retention.

The infrastructure itself may be well sized while the observability bill grows every month. Before migration, define what telemetry is required operationally and what is merely useful during the transition. Debug-level logging that makes sense during the first two weeks may not make sense six months later.

Retention should also reflect value. Security logs might need long retention, while verbose application logs may not.

Network architecture deserves economic review too

Data transfer is another area where technically correct architecture can become financially inefficient.

Cross-availability-zone traffic, NAT gateways, internet egress, replication, backups, and traffic between services can create material costs once workload volume increases. A common migration mistake is treating networking exclusively as a reliability and security problem when it is also an economic problem.

Suppose an application processes 40 TB of traffic per month between services. A topology that unnecessarily sends a large share of that traffic across zones may be perfectly functional and highly available while quietly adding thousands of euros to the bill.

The goal is not to eliminate cross-zone traffic because high availability often requires it. The goal is to understand which transfers are intentional and which are simply consequences of architecture nobody has looked at economically.

Define an infrastructure efficiency baseline before migration

One of the most useful things a team can do is capture an efficiency baseline before the migration begins.

Document the current workload, traffic, utilization, and cost as well as you can. Then define the metrics you expect to monitor in the cloud. Useful examples include:

  • cost per active customer
  • infrastructure cost per transaction
  • compute cost per request
  • database cost per customer
  • storage growth per month
  • observability cost per service
  • production vs. non-production cost
  • CPU and memory utilization by workload
  • cloud spend as a percentage of revenue

This gives the team a way to distinguish an expensive migration from an expensive architecture. If the new platform costs 30% more but improves availability significantly, reduces operational overhead, supports twice the traffic, and removes major business risk, the migration may be a success.

If the new platform costs 80% more while serving the same workload with comparable reliability, there is probably more work to do.

Without a baseline, it is difficult to tell.

Make optimization part of the migration plan

Many migration plans have phases that look roughly like this:

  1. Design the target architecture.
  2. Build the cloud platform.
  3. Migrate applications and data.
  4. Validate production.
  5. Decommission the data center.
  6. Re-evaluate sizing with real cloud workload data.

That sixth step should be explicit from the beginning, not introduced eighteen months later when finance asks why AWS is expensive.

For the first four to twelve weeks after migration, collect enough data to challenge the assumptions made during design. Review whether instance sizes match actual utilization, Kubernetes requests match workload behavior, autoscaling minimums are justified, staging and development reflect their real usage, databases were sized correctly, storage tiers make sense, observability volume is intentional, network-transfer costs match architecture expectations, idle resources remain, and commitment opportunities are becoming predictable.

A migration should not end when the application runs successfully in AWS. It should end when the team understands how that application behaves economically in AWS.

Create an operational excellence process around efficiency

The strongest long-term defense against infrastructure drift is to make efficiency part of operational excellence rather than treating it as a one-time cost project.

Engineering teams already review latency, errors, incidents, saturation, queue depth, database health, and availability. Infrastructure efficiency deserves similar attention, because the same operational data can show both reliability risk and persistent over-provisioning.

A recurring monthly or quarterly review might include CPU and memory p50, p95, and peak versus provisioned capacity; Kubernetes requests compared with actual usage; replica behavior and autoscaling boundaries; node-pool utilization and unused allocatable capacity; database CPU, memory, connections, IOPS, and storage growth; non-production spend relative to production; observability cost and data volume; network and egress cost trends; and cost per meaningful business or technical unit.

The important part is to look at both ends of the spectrum. A database running continuously above 85% CPU deserves investigation because it may become a reliability issue. A database provisioned with 32 vCPUs that rarely exceeds 15% also deserves investigation because it may be carrying unnecessary cost.

The output of this review should stay small and actionable. Resize this database, reduce this node-pool minimum, investigate this memory trend, change this autoscaling target, or deliberately leave this workload alone because the additional headroom is justified.

Infrastructure efficiency becomes much easier to sustain when it is part of operating the platform well rather than something triggered by an unexpectedly large invoice.

Prioritize savings by impact, confidence, and effort

Another mistake is turning cloud optimization into a backlog of hundreds of recommendations. That rarely works because engineering teams already have enough backlogs.

A €70-per-month optimization requiring two engineers to redesign a stateful service is probably not interesting. A €4,000-per-month database that can safely move down one tier with a maintenance window may deserve immediate attention.

I like evaluating opportunities across three dimensions:

  1. Impact: how much could this realistically save?
  2. Confidence: how strong is the evidence that the change is safe and beneficial?
  3. Effort and risk: how difficult is the change, and what could go wrong?

This produces a very different optimization backlog from a simple list ordered by theoretical savings. The objective is not maximum infrastructure utilization; it is finding the small number of changes that are economically meaningful and technically sensible.

Use the migration to change the operating model

A cloud migration is an opportunity to modernize architecture, but it is also an opportunity to change how infrastructure decisions are made.

The most expensive outcome is not necessarily choosing the wrong instance type on day one, because that can usually be fixed. The bigger risk is bringing the old infrastructure mindset into the new platform: sizing for distant future growth, treating every capacity decision as permanent, copying production sizing into every environment, and only reviewing efficiency when costs become painful.

Cloud-native infrastructure works best when teams take advantage of the fact that many decisions are reversible. Measure first, make the smallest reasonable commitment, define intentional headroom, monitor the workload, revisit assumptions, increase capacity when the evidence requires it, and reduce capacity when the workload no longer needs it.

The goal is not to build the smallest possible infrastructure. It is to build infrastructure that fits the workload you actually have while making it easy to become larger when the workload genuinely requires it. That is a very different definition of “being safe,” and usually a much cheaper one.

Summary

A successful cloud migration is not just about getting workloads running reliably in AWS, GCP, or Azure. It is also about making sure the new platform is sized around the workload you actually have rather than the infrastructure assumptions you carried from the data center.

The practical approach is straightforward: measure before migrating, distinguish reversible decisions from hard-to-change ones, define headroom deliberately, treat production and non-production differently, revisit scaling assumptions once real cloud data is available, and make efficiency part of operational excellence.

The goal is not to minimize infrastructure at all costs. It is to build a platform that is reliable, scalable, and intentionally sized — without paying today for capacity you may never need tomorrow.

Keep reading

Stop reading about savings — find yours

Connect read-only and Lizrd surfaces your highest-impact fixes, with the exact change to make.