all posts

PandaStack vs Azure Container Apps: an honest head-to-head

Ajay Kumar··10 min read

Search engines put these two things next to each other because both of them will run a workload on demand, scale it to zero, and bill you for what you used. That is roughly where the resemblance ends. Azure Container Apps is a managed serverless-container platform — Kubernetes, KEDA, Dapr and Envoy assembled into something you do not have to operate — built to run the services your team writes. PandaStack is a Firecracker microVM platform built around the assumption that the code being executed might be actively hostile.

I'm Ajay; I build PandaStack, and I get asked this comparison enough that a vague marketing answer would be worse than no answer. So here is the real one, including the substantial part where Azure Container Apps is the correct choice and I would tell you to use it.

Disclosure: I'm the founder of PandaStack, one of the two products below, so read this as a vendor's comparison. I keep it honest the only way that works: concrete numbers appear only for PandaStack, and Azure Container Apps is described qualitatively from its documented architecture. Azure ships changes weekly — pricing, quotas, workload profiles and networking modes all move — so verify anything load-bearing against the current Azure docs before you design around it.

They are not actually competing for the same job

Azure Container Apps exists to answer a specific, extremely common question: I have a container image containing code my team wrote, I want it running behind HTTPS with autoscaling, revisions, traffic splitting and a health probe, and I do not want to own a Kubernetes cluster to get that. It answers that question very well. Revisions and weighted traffic splitting give you canaries without a service mesh. KEDA gives you scaling on queue depth rather than CPU, which is the metric that actually matters for event-driven work. Dapr gives you pub/sub and state without hand-rolling it. Scale to zero means a low-traffic internal service costs approximately nothing between requests.

Every one of those features rests on something quiet and load-bearing: the code in the container is yours. Written by your team, reviewed in your PR process, built by your pipeline, and not trying to escape. Under that assumption a container is a perfectly good unit — it is a packaging and resource-limits boundary, and packaging is genuinely the problem you had.

The hard case is the other one. An LLM generated a Python script thirty milliseconds ago and you are about to execute it. A customer uploaded a plugin. A student submitted a solution. A CI job is running a fork's build script with your registry token in its environment. What you need there is not packaging. It is a boundary that holds while the code on the other side is trying to get through it — a different engineering problem with a different answer.

A container is a polite suggestion to the kernel. It works because nearly everyone is polite.

The shared kernel, and what Azure can and cannot do about it

Azure Container Apps runs containers, and containers on any platform — Azure, AWS, GCP, your laptop — share the host kernel. Namespaces and cgroups partition what a process can see and consume; they do not give it its own kernel. The surface an untrusted process gets is the full Linux syscall interface, hundreds of entry points wide, plus every ioctl the runtime left reachable. A kernel bug on that surface is a host compromise, and the mitigation is patching, not architecture.

This is not a knock on Azure specifically — it is a property of the abstraction, and Azure has built real controls on top of it. They are worth naming precisely, because each solves a genuine problem and none of them solves this one:

  • Managed identities and Entra ID scoping — excellent at limiting what a compromised workload can then reach in your Azure estate. This is the highest-value control on the list and it is real defence in depth. It does not narrow the syscall surface between the container and the host kernel.
  • VNet integration and network policy — keeps a workload off the network segments it has no business on, which contains lateral movement. It is a control on where the code can talk, not on what the code can do to the machine it is running on.
  • Workload profiles, including dedicated ones — gives you dedicated compute so you are not sharing a host with other Azure tenants. That fixes the noisy-neighbour and cross-customer question, and it is the right lever for a compliance conversation. It does not stop one of your own untrusted workloads from reaching your host kernel, because it is still a shared kernel — now shared exclusively with your other untrusted workloads.
  • Resource limits and probes — protect availability, not confidentiality. A CPU quota is a bill control and a fairness mechanism, not a security boundary.

A microVM inverts the default. Each PandaStack sandbox is a Firecracker VM with its own guest kernel, isolated by hardware virtualisation via KVM. The guest kernel is the thing that takes the untrusted syscalls, and if it is compromised, the attacker owns a VM that is about to be deleted. The interface exposed to the host is the VMM's small device surface rather than the whole Linux ABI. That is the entire pitch, and it is worth exactly as much as your threat model says it is worth — which for your own reviewed microservices is close to nothing, and for arbitrary generated code is close to everything.

Cold start, and the per-request isolation question

Here is where the architectural difference stops being philosophical and starts showing up on an invoice. Ask both platforms the same question: can I give every single request its own fresh, isolated execution environment, and throw it away afterwards?

For a container platform, the honest answer is usually no, and the reason is cold start. Starting a replica means scheduling it, pulling or mounting image layers, starting the container, and waiting for your application runtime to initialise — Node, the JVM, Python imports, connection pools. That total is fine at deploy time and painful in a request path, which is why every container platform, Azure Container Apps included, gives you a min-replicas knob. Set it above zero and cold start disappears, because you are now paying to keep instances warm.

That is a reasonable trade and I want to be fair to it: for a steady-traffic API, warm replicas are the right call and the cost is trivially justified. But notice what it does to the scale-to-zero story. The moment you pin min-replicas at one to protect your p99, you have an always-on bill, and the headline property — pay nothing when idle — now applies to everything except the services you cared about latency for. If you were hoping to use a replica per tenant as an isolation unit, multiply that idle cost by tenant count and the model fails economically long before it fails technically.

PandaStack takes the other route: make creation cheap enough that warm pools are unnecessary. There is no warm pool of idle VMs. Every create restores a baked Firecracker snapshot — a kernel already booted, runtime already initialised, guest agent already running — at roughly 179ms p50 and 203ms p99, the restore step itself around 49ms. Only a template's first-ever spawn cold-boots, in about 3 seconds, and that is what bakes the snapshot.

The consequence matters more than the number. At sub-200ms, a fresh VM per request is affordable, so per-request isolation stops being a luxury architecture and becomes the default one. Billing is per-second at $0.054 per active vCPU-hour and $0.0162 per GiB-hour, so a sandbox that lives four seconds costs four seconds — and idle costs approximately zero, because nothing is idling.

The two mental models, in code

Nothing conveys the difference faster than the deploy call. Azure Container Apps asks you to describe a long-lived service and its scaling envelope:

# Azure Container Apps: you declare a SERVICE. The platform keeps it alive,
# scales it between min and max, and routes traffic to it.

az containerapp env create \
  --name acme-env \
  --resource-group acme-rg \
  --location eastus

az containerapp create \
  --name acme-api \
  --resource-group acme-rg \
  --environment acme-env \
  --image ghcr.io/acme/api:1.4.2 \
  --target-port 8080 \
  --ingress external \
  --min-replicas 0 \
  --max-replicas 20 \
  --cpu 0.5 --memory 1.0Gi \
  --env-vars "NODE_ENV=production"

# min-replicas 0 is the scale-to-zero setting, and also the cold-start
# setting. Raising it to 1 buys latency with an always-on bill. That is
# the trade the whole model is built around -- and for YOUR OWN service,
# it is usually the correct trade.

# Revisions + weighted traffic splitting are genuinely good here:
az containerapp ingress traffic set \
  --name acme-api --resource-group acme-rg \
  --revision-weight acme-api--rev1=90 acme-api--rev2=10

PandaStack asks you to describe a machine you want for a moment. There is no service, no replica count, and nothing to keep warm — the unit of work is a VM whose whole life is one job:

# PandaStack: you declare a MACHINE, for as long as one job needs it.
# No replicas, no warm pool, no min instances. Create, use, destroy.
from pandastack import Sandbox

def run_generated_code(source: str, csv_bytes: bytes) -> dict:
    # ~179ms p50 to a fresh VM with its own guest kernel. The model wrote
    # this code seconds ago; nobody has read it; that is fine, because the
    # blast radius is a VM that is about to stop existing.
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
    try:
        sbx.filesystem.write("/work/in.csv", csv_bytes)
        sbx.filesystem.write("/work/main.py", source)

        r = sbx.exec("cd /work && python main.py", timeout_seconds=120)
        if r.exit_code != 0:
            return {"ok": False, "stderr": r.stderr[-8000:]}

        return {"ok": True, "result": sbx.filesystem.read("/work/out.json")}
    finally:
        # Per-second billing means the cost of this request is the seconds
        # it actually ran. There is no idle instance left behind to pay for.
        sbx.kill()

Neither snippet is a trick. They are the idiomatic call for each platform, and the shape of each one tells you what it was designed for. One provisions capacity that persists and serves many requests; the other provisions a boundary that exists for one.

Fork and snapshot: the capability with no container equivalent

Azure Container Apps can restart a container, roll to a new revision, and mount persistent storage. What it cannot do — what no container platform can meaningfully do — is take a running machine, with its process table, its loaded model, its warmed caches and its half-finished work, and produce two of it.

Firecracker snapshots make that a normal operation. A snapshot captures guest memory and device state; a fork restores from it with copy-on-write memory and a reflinked rootfs, so the child shares pages with the parent until it writes. Same-host forks land in 400–750ms; cross-host forks, which have to move bytes between machines, run 1.2–3.5s.

What that unlocks is narrower than the demos suggest, and I would rather say so than oversell it. It is transformative for roughly three shapes. Branch-and-explore agents: drive an agent to a decision point once, fork five children to try five approaches against identical state, keep the one that worked — the expensive setup is paid once. Per-PR database branches: fork a warmed Postgres VM so each pull request tests against a real database with real data in under a second, rather than waiting 30–90s for a fresh managed Postgres to bootstrap. And deterministic replay of an incident from the exact machine state that produced it.

If your workload is none of those, forking is a party trick and you should weight it at zero. Most services genuinely do not need it, and buying a platform for a capability you will not use is how teams end up on infrastructure that is worse at the thing they do every day.

Ecosystem gravity: where Azure Container Apps wins outright

Now the part a vendor comparison usually skips. If your organisation already lives in Azure, Azure Container Apps is very likely the right answer, and a better isolation primitive is not enough to change that. This is not a consolation paragraph — it is the most decisive factor for most readers of this post.

Consider what the integration actually buys. Managed identity means your service authenticates to Azure SQL, Key Vault and Storage with no credential in your code, no secret in a pipeline variable, and no rotation runbook. That is not a convenience feature: removing long-lived secrets from an estate is a security win that plausibly outweighs the kernel boundary for a workload running code you wrote anyway. VNet integration puts your app inside the perimeter your security team already designed and signed off. Azure Monitor sends your logs and traces where every runbook already points. Add revisions with weighted traffic splitting for canaries, KEDA scalers on Service Bus depth or Event Hubs lag, Dapr for state and service invocation, and Bicep or Terraform next to the rest of your infrastructure.

Then the human factor, which architecture diagrams never show: your team knows Azure, procurement is done, the compliance boundary is drawn, and on-call has the right access. Adding a second cloud vendor to a working estate has a recurring cost people systematically underestimate. Plainly — if the code you run is your own and you are already in Azure, use Azure Container Apps. You will ship faster, your auditors will be happier, and the isolation difference is buying protection against a threat you do not have.

Side by side, on the dimensions that decide it

  • Isolation boundary — Azure Container Apps: a container. Namespaces and cgroups on a shared host kernel, hardened with identity scoping, network policy and optionally dedicated workload profiles. PandaStack: a Firecracker microVM with its own guest kernel, isolated by KVM, so untrusted syscalls hit the guest and never the host.
  • Cold start — Azure Container Apps: schedule, pull, start, initialise your runtime; the documented mitigation is min-replicas above zero, which trades idle cost for latency. PandaStack: no warm pool at all — every create restores a baked snapshot at ~179ms p50 and ~203ms p99, with a ~3s cold boot only on a template's first-ever spawn.
  • Per-request isolation cost — Azure Container Apps: a replica per request is not the intended shape; you share replicas between requests and rely on your application to keep tenants apart. PandaStack: a VM per request is the intended shape, billed per second at $0.054 per active vCPU-hour and $0.0162 per GiB-hour, so idle is approximately zero.
  • Fork and snapshot — Azure Container Apps: no equivalent; you get restarts, revisions and persistent volumes. PandaStack: copy-on-write forks of a running machine in 400–750ms same-host and 1.2–3.5s cross-host, which is decisive for branch-and-explore agents and per-PR database branches and irrelevant for everything else.
  • Networking model — Azure Container Apps: an environment with ingress, revisions and traffic splitting, integrated with your VNet, private endpoints and the Azure network perimeter your team already runs. PandaStack: a dedicated network namespace per sandbox from a pool of 16,384 pre-allocated /30 subnets per agent, with egress controlled per sandbox — built for containment rather than for service topology.
  • Ecosystem integration — Azure Container Apps: a first-class citizen of Azure. Entra ID, managed identities, Key Vault, Azure Monitor, Bicep, and a support contract you probably already have. PandaStack: a focused API with Python and TypeScript SDKs; it integrates with your app, not with your cloud's identity plane.
  • Self-host and portability — Azure Container Apps: managed only, on Azure; portability comes from your image being a standard OCI artifact, not from the platform. PandaStack: the core is Apache-2.0 and self-hostable on any Linux host with /dev/kvm — which is a genuine exit, and also genuinely a fleet to operate, not a Compose file.

Choose Azure Container Apps if / choose PandaStack if

This is the section worth acting on. Most readers land clearly on one side.

  1. Choose Azure Container Apps if the code you run is code your team wrote and reviewed. That is the assumption its entire security model is built on, and when the assumption holds, the model is sound.
  2. Choose Azure Container Apps if your data, identity provider, compliance boundary or network perimeter is already in Azure. Managed identity removing secrets from your codebase and VNet integration keeping traffic inside an audited perimeter are worth more, in practice, than a stronger kernel boundary you do not need.
  3. Choose Azure Container Apps if you are running long-lived HTTP services or event-driven consumers with steady traffic — the KEDA scalers, revisions and weighted traffic splitting are exactly the right tools, and rebuilding them elsewhere is wasted quarters.
  4. Choose PandaStack if you execute code you did not write: LLM-generated scripts, customer plugins, student submissions, untrusted CI for forked pull requests, or anything where a security reviewer has asked what happens when this escapes.
  5. Choose PandaStack if you want a fresh environment per request or per tenant and the economics of keeping a warm replica per tenant do not work — which they usually do not past a few dozen tenants.
  6. Choose PandaStack if you specifically need to fork a running machine: branch-and-explore agents, per-PR database branches, or replaying an incident from exact captured state.

And a real pattern worth naming: choosing both is often correct and is not a cop-out. Keep your product — the API, the web app, the queue consumers — on Azure Container Apps, where the identity and networking integration earns its keep every day. Push only the untrusted-execution slice out to microVMs, called over HTTPS from your Azure workload with a scoped token. Your compliance boundary stays where your auditors expect it; the code you cannot vouch for runs somewhere it cannot hurt you. The isolation problem is usually one endpoint in your architecture, not your whole architecture.

I have deliberately not quoted Azure pricing, SLA figures, quota limits or cold-start benchmarks, because those change and a stale number is worse than none. Workload profiles, networking modes and scaling behaviour all move faster than any comparison post. Check the current Microsoft documentation and run your own numbers before a decision this size.

Frequently asked questions

Is Azure Container Apps safe for running untrusted or AI-generated code?

It depends entirely on what you mean by untrusted. Azure Container Apps gives you real, valuable controls — Entra ID scoping, managed identities, VNet integration and dedicated workload profiles — that meaningfully limit what a compromised workload can reach across your estate. What it cannot change is that containers share the host kernel, so the code is exposed to the full Linux syscall surface and a kernel bug is a host issue rather than an application one. For code your team wrote, that is a fine boundary. For code an LLM generated thirty milliseconds ago or a customer uploaded, a hardware-virtualised boundary is the honest answer, and that means microVMs.

Does scale to zero on Azure Container Apps actually mean I pay nothing when idle?

It does when min-replicas is zero and no traffic arrives. The catch is that min-replicas is also your cold-start control, so the services where latency matters are precisely the ones you end up pinning above zero — and those now have an always-on bill. That is a sensible trade for a steady-traffic API. It stops working if you were hoping to use a warm replica per tenant as an isolation unit, because the idle cost then multiplies by tenant count. Check current Azure pricing directly; I am describing the shape of the trade, not quoting rates.

Can I get sub-second per-request isolation without keeping instances warm?

Yes, if the platform restores from a snapshot instead of booting. Booting a VM per request is roughly a three-second tax, which is why the industry reached for warm pools in the first place. Restoring a pre-baked Firecracker snapshot — a kernel already up, runtime already initialised, guest agent already running — lands in the low hundreds of milliseconds. On PandaStack that is about 179ms p50 and 203ms p99, with no warm pool behind it, which makes a fresh VM per request affordable rather than aspirational.

Should I migrate off Azure Container Apps to PandaStack?

Almost certainly not wholesale, and I would say the same if you were paying me. If Azure Container Apps is running your own services well inside your Azure estate, migrating trades a working setup with managed identity and VNet integration for a marginal gain against a threat you may not have. The migration that does make sense is partial: carve out the specific slice that executes untrusted code, run that on microVMs, and call it over HTTPS from your existing Azure workload. You keep the ecosystem benefits and fix only the part that is actually broken.

What does forking a microVM give me that a container restart does not?

A fork duplicates a running machine — memory contents, process table, warmed caches, loaded models — using copy-on-write, so the child starts from exactly where the parent was rather than from an entrypoint. Same-host forks take 400–750ms. That is what makes branch-and-explore agents practical, because you pay for expensive setup once and then try five approaches from identical state, and it is what makes per-PR database branching fast, because you fork a warmed Postgres VM instead of bootstrapping a new one. A container restart always begins at process start, so there is no equivalent operation.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.