all posts

PandaStack vs AWS Lambda for Running Untrusted / LLM-Generated Code

Ajay Kumar··10 min read

Every few weeks somebody asks me the same question in a slightly different costume: "can't I just use Lambda for this?" The "this" is always running code an LLM wrote — a data-analysis snippet, a generated migration script, a tool call that resolved to `subprocess.run(whatever_the_model_decided)`. Lambda is the default answer everyone reaches for, and it deserves that status. It is one of the most operationally impressive pieces of infrastructure ever shipped. So I want to answer the question properly rather than dismissively, because the honest answer is not "Lambda is insecure." It's more interesting than that.

I'm Ajay. I built PandaStack, which is a Firecracker microVM sandbox platform, so I have an obvious commercial interest in you reaching a particular conclusion. Disclose that up front, weigh what follows accordingly, and hold me to the standard I'm about to apply to AWS: I'll be concrete about PandaStack's numbers and behavior, qualitative about Lambda's, and I will not quote limits, timeouts, or prices as fact — those change, and by the time you read this some of what I believe about them may be stale. Check the current AWS documentation before you make an architectural decision on the basis of any blog post, including this one.

The isolation argument is over, and AWS won it

Let's kill the bad version of this comparison immediately. Firecracker is an AWS project. AWS wrote it, open-sourced it, and runs Lambda on it. Every Lambda execution environment is a Firecracker microVM with its own guest kernel behind a hardware-virtualization boundary. When I tell you PandaStack gives you a real VM boundary rather than a shared-kernel container, I am describing a design AWS invented and then handed to the industry under Apache 2.0. My entire platform is built on their hypervisor. Anyone selling you "microVM isolation, unlike Lambda" is either confused or hoping you are.

So the boundary is not the differentiator. What differs is what sits on top of it — the programming model, the lifecycle you're allowed to control, and the assumptions the platform makes about who wrote the code. Lambda's model assumes you wrote the function, you tested it, you own it, and the platform's job is to run it a hundred million times as cheaply as possible. That assumption is correct for the overwhelming majority of Lambda workloads. It is a slightly awkward fit for the case where the code arrived thirty seconds ago from a language model and nobody, including the model, is confident about what it does.

Frame the choice as "which programming model fits code of unknown provenance," not "which has better isolation." Both sit on Firecracker. If someone tells you Lambda's sandbox is weak, ask them what they think Lambda runs on.

Where Lambda is genuinely excellent

I want this section to be long enough that you believe the rest of the post. Lambda is not a compromise you accept; for a huge class of work it is simply the correct answer, and the reasons are structural rather than marketing.

  • Scale you will never personally exhaust. Lambda absorbs traffic patterns that would be a capacity project anywhere else, without you filing a ticket, pre-warming a fleet, or knowing how many hosts exist.
  • There is no infrastructure. Not "managed infrastructure" — none. No host fleet to patch, no agent process, no kernel CVE that becomes your weekend. That saving is easy to undervalue until you've run hosts yourself.
  • IAM is the real thing. Per-function execution roles and an authorization model adversarially reviewed at a scale nothing else in this space approaches. Scoping a function to exactly one S3 prefix is a solved problem.
  • The event-source ecosystem is unmatched. S3, SQS, EventBridge, DynamoDB streams, Kinesis, API Gateway and more wire straight in, with retries, dead-letter queues, and concurrency controls someone else already got right.
  • Per-millisecond billing with genuine scale-to-zero. Idle costs nothing. For spiky, short work the economics are excellent and hard to beat by building it yourself.
  • A real hardware-virtualized boundary per execution environment. Firecracker, from the people who wrote Firecracker. Not a consolation prize — the strongest part of the story.

If I were building an event-driven pipeline with first-party code — transform an object when it lands in a bucket, fan a queue out to workers, back an HTTP API with functions I wrote — I would use Lambda and I would not think about it again. Building that yourself on microVMs would be a self-inflicted wound. None of what follows is an argument against Lambda in general. It's an argument about one specific workload.

Where the model fights you for untrusted code

Now the specific workload: you have code of unknown provenance and you want to run it, get output back, maybe run more code that depends on what happened, and be confident that nothing leaks between one run and the next. Here's where Lambda's design — which is correct for its intended use — starts pulling against you.

Execution environments are reused, deliberately

This is the big one and it is not a bug. Lambda keeps an execution environment warm after an invocation and routes a subsequent invocation of the same function to it. That reuse is exactly why warm invocations are fast and why the initialization-outside-the-handler pattern exists — you open your database pool once and amortize it across many requests. It's good engineering. The boundary is between *functions and tenants*, not between *invocations of the same function*.

Which is fine when every invocation runs your code. It becomes a genuine concern when invocation N runs user A's code and invocation N+1 runs user B's, because the platform has no idea those are different trust domains — from Lambda's perspective it's the same function twice. Anything the first run left behind is still there: files in the writable temp directory, module-level globals, a background thread it spawned, a monkeypatched standard library function, a file descriptor it forgot to close. An execution environment that helpfully remembers the last tenant's temp files is a lovely optimization and a genuinely uncomfortable multi-tenancy story.

The failure mode here isn't a sandbox escape — it's cross-invocation contamination inside a boundary that was never meant to separate invocations. Attacker-influenced code that writes a file to the temp directory and waits is not exploiting a vulnerability. It's using the runtime exactly as designed, against a threat model the runtime doesn't hold.

You can engineer around this — wipe the temp directory on entry and exit, run the payload in a subprocess and reap it, never carry mutable state across the handler boundary. It's doable, and some teams do it well. But notice what you're doing: fighting the platform's central optimization on every invocation, forever, with discipline as the only enforcement mechanism. Discipline is not a security boundary. It's a thing that works until the on-call engineer who understood why ships a shortcut at 2am.

Duration, and the shape of the deployment unit

Lambda has a hard maximum execution duration. I won't state the current number — it has changed before and will change again, so verify it against AWS's docs. The architectural point holds regardless of the value: there is a ceiling, and an agent working a real task can walk into it. Cloning a large repository, installing a dependency tree, running a test suite, iterating on failures — that's not a pathological workload, it's Tuesday. When you hit the ceiling you get a truncated task and a checkpointing problem you now own.

The deeper mismatch is the deployment unit. Lambda's unit is a packaged function or container image: you decide ahead of time what code exists, you build it, you deploy it, and then you invoke it. The agent use case is the inverse. You don't know what the code is until the model emits it, and what you want is closer to "here is a shell, run this command." You can bridge the gap — deploy a generic executor function that accepts a code string and `exec`s it — and people do. But you've now built an arbitrary-code-execution endpoint by hand, inside a reused environment, and made yourself responsible for the isolation the deployment-unit design was implicitly providing.

No terminal, no fork, no machine to hold onto

A short list of primitives that agent workloads reach for and that Lambda's model doesn't hand you:

  • No interactive PTY. You cannot attach a terminal to an execution environment and drive it, which rules out the human-in-the-loop "let me look at what the agent did" workflow and anything that needs a real tty.
  • Arbitrary runtimes and system packages mean building images. Want a specific Python, plus ffmpeg, plus a headless browser, plus whatever the model decided it needs at runtime? That's a build-and-deploy cycle, not an `apt-get` inside a live machine.
  • No user-facing fork-the-machine primitive. SnapStart exists and is a genuinely clever piece of engineering for cutting initialization cost on cold starts — but it targets that problem, not "branch this running machine mid-execution into three variants and keep the best." Check AWS's current docs for what SnapStart does and doesn't expose; it is not the branch-and-explore API an agent search loop wants.
  • Egress control is VPC-shaped. You get network controls at the VPC/security-group level, which is the right granularity for an application tier and a coarse one when you want per-run rules for a specific piece of generated code.
  • Statefulness across a multi-turn session must be externalized. The agent's working directory, its installed packages, its half-finished build — all of it has to live in S3 or EFS or a database and be rehydrated, because the environment underneath is not yours to keep.

That last one is the quiet tax. A lot of "we built an agent sandbox on Lambda" architectures are mostly a state-rehydration layer with a function attached, and the rehydration layer ends up being the majority of the code and nearly all of the bugs.

What PandaStack does differently

PandaStack's bet is narrow: expose the machine rather than the function. A sandbox is a Firecracker microVM you create, exec into, read and write files on, snapshot, fork, pause, resume, and kill. It's the same isolation primitive Lambda uses, with the lifecycle handed to you instead of managed on your behalf.

The objection to that has always been cost: a VM per task sounds expensive if a VM means a multi-second boot. Snapshot-restore is what changes the arithmetic. There is no warm pool of idle VMs — every create restores a baked template snapshot on demand. The restore step lands around 49ms, end-to-end create is p50 179ms and p99 around 203ms, and only the very first cold boot of a template costs about 3 seconds. Fresh machine per task stops being a luxury and becomes the cheap default, which is the whole point: if a clean machine is cheap, you never have to reuse a dirty one.

Forking is the primitive I'd most want if I were building an agent search loop. A same-host fork is 400–750ms and a cross-host fork is 1.2–3.5s, using copy-on-write for both memory and disk. Set up an environment once — repo cloned, dependencies installed, database seeded — then branch it three ways, run a different candidate fix in each, and keep whichever passes. Networking is a per-sandbox namespace, with 16,384 pre-allocated /30 subnets per agent host, so "what can this run reach" is a per-sandbox question rather than a VPC-wide one. And when the agent needs a real database, managed Postgres creates in 30–90 seconds.

What the difference looks like in code

Here's the Lambda-shaped version of an untrusted-code executor. I've written it about as carefully as a person reasonably can, and I want you to look at how much of it is defensive housekeeping rather than the actual job.

# lambda_function.py — the "generic executor" pattern, done carefully.
# Every line below the handler signature exists because the execution
# environment may have already run somebody else's code.
import os, shutil, subprocess, sys, tempfile, uuid

SCRATCH = "/tmp/run"   # writable, and NOT guaranteed empty on entry

def _scrub():
    """Wipe anything the previous invocation left behind."""
    shutil.rmtree(SCRATCH, ignore_errors=True)
    os.makedirs(SCRATCH, mode=0o700, exist_ok=True)

def handler(event, context):
    _scrub()                                  # before...
    try:
        src = os.path.join(SCRATCH, f"{uuid.uuid4()}.py")
        with open(src, "w") as f:
            f.write(event["code"])            # written by an LLM. good luck.

        # Subprocess, not exec(): a crash or a monkeypatched stdlib in the
        # payload must not contaminate the long-lived handler process.
        p = subprocess.run(
            [sys.executable, src],
            capture_output=True, text=True,
            cwd=SCRATCH,
            timeout=min(60, context.get_remaining_time_in_millis() / 1000 - 5),
            env={"PATH": "/usr/bin:/bin", "HOME": SCRATCH},  # rebuild env
        )
        return {"stdout": p.stdout, "stderr": p.stderr, "exit_code": p.returncode}
    except subprocess.TimeoutExpired:
        # Note: the *function's* hard duration ceiling is a separate, lower
        # bound than anything you set here. A long agent task hits it.
        return {"stdout": "", "stderr": "timed out", "exit_code": 124}
    finally:
        _scrub()                              # ...and after. Both. Always.

# Still unhandled: background threads the payload spawned, sockets it opened,
# anything it wrote outside SCRATCH, and the next tenant inheriting this same
# warm environment in a few milliseconds.

That code is not bad. It's what a competent engineer writes. But `_scrub()` is doing security work with a filesystem call, and the comment at the bottom is the honest part: the residue you can't reach from inside the process is still there when the next invocation arrives. Now the PandaStack version of the same job.

from pandastack import Sandbox

def run_untrusted(code: str) -> dict:
    # A brand-new microVM, restored from a baked snapshot. ~49ms restore,
    # p50 179ms end to end. Nothing in it has ever seen another tenant.
    # ttl_seconds is the backstop: if this process dies, the VM still reaps.
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
    try:
        sbx.filesystem.write("/work/main.py", code)
        r = sbx.exec("cd /work && python main.py", timeout_seconds=60)
        return {"stdout": r.stdout, "stderr": r.stderr, "exit_code": r.exit_code}
    finally:
        # Not a cleanup routine — the machine ceases to exist. Background
        # threads, open sockets, stray files, a scribbled-on stdlib: all of
        # it dies with the guest kernel, because it was never anywhere else.
        sbx.kill()


# Multi-turn: the agent keeps ONE machine across the whole session, so
# `pip install` in turn 1 is still installed in turn 7. No rehydration layer.
def agent_session(steps: list[str]) -> list[str]:
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=3600)
    out = []
    try:
        for step in steps:
            r = sbx.exec(step, timeout_seconds=600)   # no platform-imposed ceiling
            out.append(r.stdout if r.exit_code == 0 else r.stderr)
    finally:
        sbx.kill()
    return out

The difference isn't line count, it's where the guarantee lives. In the Lambda version, isolation between two users' code is a property of my scrub function being correct and being called on every path. In the PandaStack version it's a property of the two runs having happened on different machines with different kernels. One of those is auditable by reading code; the other is true by construction.

# The lifecycle Lambda's model doesn't expose, from the CLI.

# 1. Set up the expensive state ONCE: clone, install, seed.
SBX=$(pandastack sandbox create --template code-interpreter --ttl 3600 -o id)
pandastack exec $SBX -- bash -lc 'git clone --depth 1 $REPO /work && \
  cd /work && pip install -r requirements.txt && pytest -x || true'

# 2. Branch it. Same-host fork is 400-750ms, so three candidate fixes
#    from an identical starting state cost about two seconds total.
for n in 1 2 3; do
  FORK=$(pandastack sandbox fork $SBX -o id)
  pandastack filesystem write $FORK /work/fix.patch < "candidate-$n.patch"
  pandastack exec $FORK -- bash -lc 'cd /work && git apply fix.patch && pytest -q'
done

# 3. Keep the winner, kill the rest. The losing branches leave nothing behind.
pandastack sandbox kill $SBX

Side by side

Read the Lambda column as "what its model is optimized for," not "what it is bad at" — and re-verify the specifics against current AWS documentation, because this is a moving target.

  • Isolation primitive — AWS Lambda: Firecracker microVM per execution environment, hardware-virtualized. PandaStack: Firecracker microVM per sandbox, hardware-virtualized. Same primitive; AWS wrote it.
  • Boundary between two untrusted runs — AWS Lambda: environments are reused across invocations by design, so separating runs is your discipline, not the platform's. PandaStack: a different VM with a different guest kernel, by construction.
  • Deployment unit — AWS Lambda: a packaged function or container image you build and deploy ahead of time. PandaStack: a machine you exec into; the command can be a string the model produced a second ago.
  • Duration — AWS Lambda: a hard maximum execution duration per invocation; long tasks need checkpointing (verify the current limit in AWS docs). PandaStack: sandbox lifetime is yours, bounded by the TTL you set and your own timeouts.
  • Interactive access — AWS Lambda: no PTY; you invoke and receive a response. PandaStack: exec, streaming exec, and a WebSocket PTY you can attach a terminal to.
  • Getting a runtime or system package in — AWS Lambda: build a layer or an image, deploy, invoke. PandaStack: install it inside the running machine, or bake it into a template snapshot once.
  • Branch-and-explore — AWS Lambda: no user-facing fork-the-machine API; SnapStart addresses cold-start init, not mid-execution branching. PandaStack: fork with copy-on-write memory and disk, 400–750ms same-host, 1.2–3.5s cross-host.
  • Network egress control — AWS Lambda: VPC-shaped — subnets, security groups, NAT. PandaStack: per-sandbox network namespace, with 16,384 pre-allocated /30 subnets per agent host.
  • Multi-turn session state — AWS Lambda: externalize it; the environment is not yours to keep. PandaStack: it's the same machine across turns, so state persists until you kill it.
  • Ecosystem and event sources — AWS Lambda: unmatched — S3, SQS, EventBridge, API Gateway, IAM, the whole surface. PandaStack: an SDK, a CLI, and a REST API. This is not close, and it's not close in AWS's favor.
  • Operational burden — AWS Lambda: none; there is no fleet. PandaStack: managed, or self-host on your own KVM hosts if you want the control.
  • Vendor risk — AWS Lambda: AWS. PandaStack: a much smaller company, which is a real thing to weigh.

PandaStack's own tradeoffs, stated plainly

A comparison where my product wins every row would be a brochure. Here's the bill.

  • vCPU and RAM are frozen at snapshot bake time. A Firecracker snapshot captures machine configuration alongside the memory image, so you cannot resize per create — you re-bake the template. That's the hypervisor, not a shortcut, and it's the direct price of the millisecond-scale restore.
  • The ecosystem gap is enormous. There is no PandaStack equivalent of EventBridge, or IAM, or twenty years of AWS integrations. If your architecture is event-driven and AWS-native, wiring PandaStack in means writing glue that Lambda would have given you free.
  • You're trusting a much smaller vendor. AWS has a scale, a compliance surface, and a durability that I do not. That is a legitimate reason to choose Lambda and I'm not going to argue you out of it.
  • You have to think about lifecycle. TTLs, kills, orphaned sandboxes — Lambda's model means never thinking about any of that. Explicit lifecycle is more control and more rope.
  • No GPUs, and no attempt at Lambda's event-source ergonomics. Different tool, different shape.

Use Lambda, honestly

Concretely, the cases where I'd tell you to close this tab and go write a function:

  • The code is yours. First-party logic you wrote, reviewed, and tested. The reuse concern evaporates entirely when every invocation runs code from the same trust domain — which is most software.
  • The work is event-driven. Something lands in a bucket, a message hits a queue, a schedule fires. Lambda's event sources plus their retry and DLQ semantics are a solved problem you should not re-solve.
  • You're already deep in AWS. If IAM roles, VPCs, and CloudWatch are how your organization thinks, the integration value is real and adding an outside platform has a coordination cost that's easy to underestimate.
  • The jobs are short and bursty. Per-millisecond billing with true idle-to-zero is excellent for spiky, brief work, and matching that on any other substrate takes real effort.
  • You need scale you can't predict. Lambda absorbs traffic shapes that would otherwise become a capacity project.
  • You want zero infrastructure. Not less — zero. That is worth a great deal and it is the single strongest argument in Lambda's favor.

And the cases where the function model genuinely fights you: the code is generated or user-submitted and you need a boundary between successive runs that doesn't depend on your cleanup being perfect; an agent task can run long enough to hit a duration ceiling; you want a shell rather than a deployment; a human sometimes needs to attach a terminal and look around; the runtime and packages are decided at runtime; or your search loop wants to branch a prepared machine three ways and keep the best result. Those are the workloads I built PandaStack for, and they're a narrow slice of all computing — a slice that happens to be growing quickly because agents write a lot of code now.

The question was never whether Lambda's sandbox is strong enough. AWS built the sandbox everyone else is using. The question is whether a deployment unit designed for code you wrote and trust is the right container for code that didn't exist when you deployed it.

Plenty of teams should run both, and the split is clean: Lambda for the event-driven, first-party plumbing that makes up most of a product, PandaStack for the per-session isolated machine where the agent runs whatever it just invented. That's not fence-sitting — it's what the architectures actually look like once you stop trying to make one primitive cover both. Just re-check AWS's current documentation before you finalize anything on the basis of a limit I deliberately declined to quote.

Frequently asked questions

Is AWS Lambda safe for running untrusted or LLM-generated code?

Lambda's isolation boundary is genuinely strong — each execution environment is a Firecracker microVM with its own guest kernel, and AWS wrote Firecracker. The concern for untrusted code isn't escape, it's reuse: Lambda deliberately keeps execution environments warm and routes subsequent invocations of the same function to them, because that's what makes warm invocations fast. The boundary separates functions and tenants, not successive invocations of one function. If invocation N runs user A's generated code and invocation N+1 runs user B's, anything left in the writable temp directory, in module globals, in a spawned background thread, or in a monkeypatched standard library is still present. You can engineer around it by scrubbing state and running payloads in a reaped subprocess, but that's discipline enforcing a security property rather than the platform enforcing it.

Does PandaStack use better isolation than AWS Lambda?

No, and I want to be unambiguous about that because the claim gets made carelessly. Both use Firecracker microVMs with hardware virtualization and a separate guest kernel per workload. It is literally the same primitive, and AWS invented it and open-sourced it under Apache 2.0. The difference is the model layered on top: Lambda gives you a function you deploy and invoke, with environments reused across invocations for performance; PandaStack gives you a machine you create, exec into, and destroy, with a fresh VM per sandbox. For code you wrote, reuse is a pure win. For code of unknown provenance, a fresh machine per run makes the separation structural instead of procedural.

Can't I just give every user their own Lambda function for isolation?

You can, and for a small, fixed set of tenants it's a defensible design — a function per tenant gets you per-tenant environment separation plus a per-tenant IAM role, which is genuinely nice. It stops scaling well when tenants are dynamic: you're now managing function lifecycle, deployment, and cleanup as a runtime concern, you inherit account-level function and concurrency limits, and cold-start behavior gets worse because each function's environments are warmed independently. It also doesn't help with the case where a single user submits two pieces of code that shouldn't see each other's leftovers. Check current AWS quotas before designing around this, since limits change.

What about SnapStart — isn't that the same as snapshot and fork?

They're related ideas aimed at different problems. SnapStart is AWS's mechanism for reducing cold-start cost by restoring from a snapshot taken after initialization, which is clever engineering and materially helps functions with expensive startup. What it isn't is a user-facing API for branching a running machine mid-execution into several independent copies you then drive separately. PandaStack's fork is that: copy-on-write memory and disk producing a live sibling sandbox in 400–750ms same-host or 1.2–3.5s cross-host, which is what an agent search loop wants when it needs to try three candidate fixes from one prepared state. Verify SnapStart's current capabilities in AWS's documentation rather than trusting my summary — it has gained features over time.

When should I use AWS Lambda instead of PandaStack?

When the code is yours. If you wrote, reviewed, and tested the function, environment reuse is a performance benefit rather than a contamination risk, and essentially every argument in this comparison collapses in Lambda's favor. Use Lambda for event-driven work wired to S3, SQS, EventBridge, or API Gateway; for short bursty jobs where per-millisecond billing and true scale-to-zero shine; when your team already lives in AWS and IAM is how you think about authorization; and whenever the appeal of zero infrastructure to operate outweighs wanting explicit control over machine lifecycle. PandaStack earns its place on a narrower slice: generated or user-submitted code, long-running multi-turn agent sessions, interactive terminal access, runtimes decided at runtime, and branch-and-explore forking.

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.