IaaS vs PaaS vs SaaS: Choosing the Right Cloud Service Model in 2026A practical guide to the cloud computing spectrum - and why serverless, containers, and BaaS are reshaping how the old model even applies

Introduction

Every engineering team that touches the cloud eventually runs into the same three-letter acronyms: IaaS, PaaS, and SaaS. They show up in vendor pitch decks, architecture reviews, and compliance questionnaires, usually presented as a tidy pyramid with "more control" at the bottom and "more convenience" at the top. That pyramid is a useful teaching tool, but it was drawn up more than a decade ago, before Lambda functions, Kubernetes-as-a-service, and Backend-as-a-Service platforms blurred the boundaries it describes. Engineers who still reason about the cloud using only these three categories tend to make architectural decisions that don't reflect how modern platforms actually bill, scale, or fail.

This article revisits the classic service model taxonomy from a practical, engineering-first perspective. Rather than repeating marketing definitions, we'll look at what each model actually delegates to the provider, where the operational burden really sits, and how newer models - Function-as-a-Service, Container-as-a-Service, and Backend-as-a-Service - fit into (and sometimes break) the original framework. The goal is to give you a decision framework you can apply the next time you're picking infrastructure for a new service, not just vocabulary for a whiteboard.

Context: Why the Service Model Distinction Still Matters

The IaaS/PaaS/SaaS taxonomy originates from the U.S. National Institute of Standards and Technology's Definition of Cloud Computing (NIST Special Publication 800-145, 2011), which formalized cloud computing around five essential characteristics - on-demand self-service, broad network access, resource pooling, rapid elasticity, and measured service - and three service models. That document predates Docker (2013), Kubernetes (2014), and AWS Lambda (2014), so it's worth remembering that everything built since has had to retrofit itself into a classification designed for an earlier era of virtualized servers and hosted software.

Despite its age, the model persists because it answers a question every engineering organization has to answer repeatedly: who is responsible for what layer of the stack, and what are we paying for that responsibility? A team choosing between running PostgreSQL on an EC2 instance versus using Amazon RDS versus using a fully managed SaaS analytics tool is really asking the same underlying question three different ways. The shared responsibility model - a concept popularized heavily by AWS's own security documentation - is the practical lens through which most cloud architecture decisions get made: as you move up the stack from IaaS to SaaS, the provider absorbs more operational responsibility, and you lose a corresponding amount of control.

The complication is that "control" is not a single dial. You can trade away operational responsibility for patching an operating system while retaining full control over your data schema, or you can adopt a SaaS product that owns both. Different teams have wildly different risk tolerances for each of these trade-offs - a fintech company handling PCI-DSS-scoped data will make different calls than a three-person startup validating a business idea - and the three-letter taxonomy doesn't capture that nuance on its own. That's why the rest of this article treats the classic models as a starting vocabulary and then layers in the more granular alternatives that have emerged to fill the gaps.

The Core Models, Explained Technically

Infrastructure-as-a-Service (IaaS) gives you virtualized compute, storage, and networking, and stops there. Amazon EC2, Google Compute Engine, and Azure Virtual Machines are the canonical examples. You choose the OS image, you patch it, you manage the network security groups, you decide how to scale it - typically via auto-scaling groups keyed on CPU or custom CloudWatch metrics. The provider's responsibility ends at the hypervisor and the physical data center; everything above that, including the guest OS kernel, is yours. This is the model closest to renting a physical server, and it's the right choice when you need low-level control - custom kernel modules, specific compliance-driven OS hardening, or workloads that don't map cleanly onto a managed runtime.

Platform-as-a-Service (PaaS) absorbs the operating system, runtime, and often the scaling logic. Heroku, Google App Engine, AWS Elastic Beanstalk, and Azure App Service are representative examples: you push application code (or a container image, in some PaaS variants), and the platform handles provisioning, load balancing, and horizontal scaling according to rules you configure. The trade-off is a narrower surface for customization - you're constrained to supported language runtimes, buildpacks, and often specific dependency versions - in exchange for a dramatically shorter path from code to production. PaaS is where most teams should default unless they have a specific reason not to, because it removes an entire category of operational toil (OS patching, base image CVEs, load balancer configuration) without removing your ability to reason about application-level behavior.

Software-as-a-Service (SaaS) delivers a complete, ready-to-use application over the network, with the provider owning everything below the UI (and often much of the UI's configuration surface too). Salesforce, Google Workspace, Slack, and Datadog are SaaS products in the classic sense - you don't provision anything; you configure and consume. From an engineering standpoint, SaaS decisions are less about infrastructure and more about integration surface: what APIs and webhooks does the product expose, how does it handle data export and portability, and what's your actual exit cost if you need to migrate away from it later. Vendor lock-in risk is highest here, which is why SaaS contracts often warrant more legal and architectural scrutiny than the technical simplicity of "just sign up" would suggest.

Layered underneath and between these three sit the modern alternatives that don't map cleanly onto the original taxonomy. Function-as-a-Service (FaaS) - AWS Lambda, Google Cloud Functions, Azure Functions - takes PaaS's abstraction one step further by removing the concept of a persistently running server entirely; you deploy a function, the platform invokes it on triggers, and you pay per invocation and execution duration rather than per provisioned hour. Container-as-a-Service (CaaS) - Amazon ECS/Fargate, Google Cloud Run, Azure Container Apps - sits between IaaS and PaaS, giving you the packaging flexibility of containers (you control the image, its base OS, its dependencies) without the burden of managing the underlying VM fleet or the orchestrator's control plane. Backend-as-a-Service (BaaS) - Firebase, Supabase, AWS Amplify - targets client-heavy applications by bundling authentication, a database, and often real-time sync into a single SDK-driven product, occupying a niche that's arguably closer to SaaS-for-developers than to traditional PaaS.

Implementation Patterns and Practical Examples

To make the trade-offs concrete, consider a common scenario: exposing an HTTP endpoint that resizes an uploaded image and stores it in object storage. Below is how the same conceptual workload looks across three service models.

On IaaS, you're responsible for the full request lifecycle, including the web server and process supervision:

// IaaS: Express server running on a self-managed EC2 instance
// You own OS patching, process management (e.g., via systemd or pm2),
// and horizontal scaling through an Auto Scaling Group.
import express from "express";
import sharp from "sharp";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const app = express();
const s3 = new S3Client({ region: "us-east-1" });

app.post("/resize", express.raw({ type: "image/*", limit: "10mb" }), async (req, res) => {
  try {
    const resized = await sharp(req.body).resize(800, 600, { fit: "inside" }).toBuffer();
    const key = `resized/${Date.now()}.jpg`;

    await s3.send(new PutObjectCommand({
      Bucket: "my-app-images",
      Key: key,
      Body: resized,
      ContentType: "image/jpeg",
    }));

    res.status(201).json({ key });
  } catch (err) {
    // On IaaS, unhandled process crashes take down the whole instance
    // until your process supervisor restarts it - you own that recovery path.
    console.error("Resize failed", err);
    res.status(500).json({ error: "resize_failed" });
  }
});

app.listen(3000);

On FaaS, the same logic drops the server entirely - the platform owns invocation, concurrency, and crash isolation per request:

# FaaS: AWS Lambda handler triggered by an S3 upload event
# No process to manage; the platform scales concurrency automatically
# and bills per invocation and per GB-second of execution.
import boto3
from PIL import Image
from io import BytesIO

s3 = boto3.client("s3")

def handler(event, context):
    record = event["Records"][0]["s3"]
    bucket = record["bucket"]["name"]
    key = record["object"]["key"]

    obj = s3.get_object(Bucket=bucket, Key=key)
    image = Image.open(BytesIO(obj["Body"].read()))
    image.thumbnail((800, 600))

    buffer = BytesIO()
    image.save(buffer, format="JPEG")
    buffer.seek(0)

    resized_key = f"resized/{key.split('/')[-1]}"
    s3.put_object(Bucket=bucket, Key=resized_key, Body=buffer, ContentType="image/jpeg")

    return {"statusCode": 200, "body": resized_key}

The FaaS version has no listening port, no supervisor, and no notion of instance count - Lambda handles retries and concurrency scaling on your behalf, though this introduces its own constraints, such as execution time limits and cold-start latency, that the IaaS version doesn't have to think about at all. On a BaaS platform like Supabase, you would likely skip writing this function altogether and instead configure a storage trigger and an edge function through the provider's dashboard, trading code ownership for configuration - a trade-off worth naming explicitly rather than treating as free.

Trade-offs and Common Pitfalls

The most consequential trade-off across every layer of this stack is the inverse relationship between control and operational leverage, but it's worth being precise about what "control" actually buys you. On IaaS, you retain the ability to install arbitrary kernel modules, tune TCP settings, or run non-standard runtimes - capabilities that matter for niche workloads like GPU-bound simulation or legacy software with unusual dependencies, but that most CRUD-backed web applications never actually exercise. Teams frequently choose IaaS out of habit or a vague sense that "more control is safer," then pay for that choice in patch management overhead and slower deployment cycles without ever using the control they preserved.

Cost modeling is another place where the taxonomy misleads people. FaaS pricing is attractive for spiky, low-average-utilization workloads because you pay per invocation rather than per provisioned hour, but it can become more expensive than a comparably-sized PaaS or CaaS deployment once request volume is sustained and high - the crossover point depends heavily on invocation duration and memory allocation, and teams should model it explicitly rather than assuming serverless is always cheaper. Martin Fowler's writing on serverless architectures (via martinfowler.com) has long emphasized that the economic and operational case for FaaS is workload-shape-dependent, not universal, and that's still true.

Vendor lock-in risk scales with how far up the stack you go, but it's not a clean linear function. A Lambda function written in idiomatic Node.js or Python with minimal use of AWS-specific SDKs can be ported to another FaaS provider or to a container relatively cheaply. A deep integration with a BaaS platform's real-time sync engine or a SaaS product's proprietary automation rules can be far harder to unwind, because the coupling isn't just at the infrastructure level - it's baked into your application's data model and business logic. Engineers evaluating a new platform should ask not "how much does this cost to adopt" but "how much does this cost to leave," and stress-test that answer against a hypothetical migration before signing a multi-year contract.

Finally, observability and debugging get harder, not easier, as you move up the stack, which is counterintuitive given how much operational burden the provider absorbs. On IaaS, you can SSH into a box and attach a debugger. On FaaS, you're dependent on the provider's logging and tracing integration (CloudWatch Logs, AWS X-Ray, or an APM vendor's Lambda layer) and cold-start behavior can obscure the difference between a slow function and a slow platform. On SaaS, you often can't observe internal behavior at all beyond what the vendor's own dashboard or audit log exposes. Teams adopting higher-abstraction models need to invest deliberately in observability tooling appropriate to that model rather than assuming their existing IaaS-era runbooks and dashboards will simply carry over.

Best Practices for Choosing a Service Model

Start from the workload's shape rather than from a philosophical preference for a particular model. A batch job that runs for two minutes once an hour is a poor fit for a persistently running IaaS instance and a strong fit for FaaS or a scheduled container task. A long-running stateful service with strict latency requirements and predictable, sustained load is often better served by CaaS or even IaaS with reserved capacity, where you can amortize cost and avoid cold starts entirely. Resist the urge to standardize on a single model organization-wide for its own sake - mature engineering organizations routinely run IaaS, PaaS, CaaS, and FaaS workloads side by side, chosen per-service based on its actual traffic and latency profile, not based on which model is currently fashionable.

Treat the shared responsibility boundary as an explicit design artifact, not an implicit assumption. For every service you deploy, write down - even informally, in an architecture decision record - what the provider is responsible for and what your team is responsible for, especially around security patching, backup and restore, and incident response. This is especially important for compliance-scoped workloads: frameworks like SOC 2 and ISO 27001 audits routinely surface gaps where a team assumed their PaaS or SaaS vendor was handling a control (like encryption key rotation or access logging) that was, in fact, left to the customer to configure.

Finally, instrument for portability from day one, even if you don't intend to migrate anytime soon. Using standard protocols (HTTP, gRPC, SQL) and avoiding provider-specific SDK calls in your core business logic - pushing them instead to a thin adapter layer - keeps your exit costs low without meaningfully slowing down initial development. This is the same principle behind the hexagonal architecture pattern (ports and adapters, as described by Alistair Cockburn) applied specifically to cloud provider dependencies: your domain logic shouldn't need to know whether its storage layer is S3, Google Cloud Storage, or a self-hosted MinIO instance.

Key Takeaways

  • Match the service model to the workload's traffic shape - spiky and infrequent favors FaaS, sustained and predictable favors CaaS or IaaS with reserved capacity.
  • Write down the shared responsibility boundary explicitly for every service, rather than assuming the provider covers more than it actually does.
  • Model total cost of ownership across the full lifecycle, including the cost of migrating away, not just the sticker price of adoption.
  • Invest in model-appropriate observability - SSH-and-debug habits from IaaS don't transfer to FaaS or SaaS environments.
  • Isolate provider-specific SDK calls behind an adapter layer so a change in service model doesn't require rewriting core business logic.

Conclusion

The IaaS/PaaS/SaaS taxonomy remains a useful starting vocabulary, but treating it as an exhaustive or static classification undersells how much the cloud landscape has diversified since NIST first codified it. FaaS, CaaS, and BaaS aren't just marketing variants of the same three ideas - they represent genuinely different points on the control-versus-leverage spectrum, each with distinct cost models, failure modes, and debugging affordances that engineers need to reason about on their own terms.

The most effective architectural decisions come from asking specific questions about a specific workload - its traffic shape, its compliance requirements, its team's operational capacity - rather than defaulting to whichever model is most familiar or most heavily marketed at the moment. Used well, this expanded framework isn't a constraint on cloud architecture decisions; it's a checklist that helps ensure the trade-offs you're making are the ones you actually intended to make.

References

  • National Institute of Standards and Technology, The NIST Definition of Cloud Computing, Special Publication 800-145 (2011)
  • Amazon Web Services, Shared Responsibility Model, AWS Documentation (docs.aws.amazon.com)
  • Amazon Web Services, AWS Lambda Developer Guide (docs.aws.amazon.com)
  • Google Cloud, Cloud Run Documentation (cloud.google.com/run/docs)
  • Microsoft Azure, Azure App Service Overview (learn.microsoft.com/azure/app-service)
  • Martin Fowler, Serverless Architectures, martinfowler.com
  • Cloud Native Computing Foundation, CNCF Cloud Native Definition v1.0 (github.com/cncf/toc)
  • Alistair Cockburn, Hexagonal Architecture (Ports and Adapters), alistair.cockburn.us
  • Firebase Documentation, Google (firebase.google.com/docs)
  • Supabase Documentation (supabase.com/docs)