Running AI Agent Sandboxes on AWS Lambda MicroVMs: A Lead Role in a Cage

Table of Contents
“Did you exchange a walk-on part in the war for a lead role in a cage?” — Pink Floyd, Wish You Were Here
Waters wrote that about selling out, but I keep hearing it every time I hand an AI agent a shell.
That’s the trade, isn’t it? You want the agent to have a lead role. Root, a real filesystem, a package manager, the ability to install something and try again when it fails. And you want all of that inside a cage, because the code it’s about to run is code nobody read first. Which is to say you want a sandbox, and the question is what that sandbox is actually made of.
I’ve been running Hermes Agent for a while now, and since the beginning of this year, OpenClaw too. OpenClaw is also what I was driving the week Bedrock’s Project Mantle briefly decided I owed AWS $58,000. That one turned out to be a token accounting bug on the AWS side rather than anything the agent did, but it left me with a habit: think about blast radius before handing a process your credentials, not after.
Both of them default to a Docker container for the cage, and Docker is fine right up until you remember that a container shares the host kernel. In my case that host is a Raspberry Pi, and it isn’t a dedicated agent box. It also runs Karakeep for my bookmarks and Immich for my photos. So a namespace escape isn’t an abstract security exercise for me. It’s every photo I’ve taken in the last ten years, sitting one kernel away from a process that writes its own code.
So when AWS shipped Lambda MicroVMs back in June, my first thought was “nice, a new compute primitive”. Over the following months it turned into “can I put my agents in there?”
Can Hermes and OpenClaw even use a custom sandbox? #
I was fairly sure Hermes could do it. It already ships backends for Modal, Daytona and Vercel Sandbox, so another cloud sandbox is clearly a shape it understands.
OpenClaw I was much less sure about. The docs list Docker, Podman, SSH and OpenShell, and nothing in the wording suggested you could bring your own. I half expected to write this article about Hermes alone.
I started exploring the codebases with the help of Claude, because I’m not a developer, and also because those repositories are becoming quite large, with a lot of design decisions not stated clearly.
The good news is that both of them can implement custom backends, and they don’t require a fork. Both have a documented extension point for this, and in Hermes source code, there’s a docstring tell us:
this extension point exists so third-party sandbox vendors do NOT have to live in core
What a Lambda MicroVM gives you that a container doesn’t #
A quick recap, because “serverless VM” is a phrase that means five different things depending on who’s selling it.
A Lambda MicroVM is a Firecracker VM with its own kernel, it starts from an image built from a Dockerfile. After you generate the image, AWS will start it, wait for your app to signal ready and then snapshot memory and disk, so processes running will still be there in every VM you launch afterwards. When you launch the VM, it will resume from that snapshot instead of cold booting.
Three properties matter for an agent sandbox:
It’s stateful. The VM keeps its filesystem and its processes between calls, for up to eight hours. That maps onto an agent session almost exactly. Your agent runs pip install, then twelve tool calls later the package is still there.
It suspends. Because you can specify an idle policy, after N seconds without traffic the VM suspends, and it will auto-resume when the next request arrives. Suspended VMs cost snapshot storage and nothing else. Given that an agent spends most of its wall-clock life waiting on a model to finish thinking, this is the feature that makes the whole thing viable.
Nothing is shared, which is the point of using an external execution environment!
The lifecycle is quite simple: RunMicrovm, SuspendMicrovm, ResumeMicrovm, in the end it will be TerminateMicrovm. States go in this way: PENDING → RUNNING → SUSPENDING → SUSPENDED → RUNNING, with TERMINATING → TERMINATED at the end.
One thing you should know, and I wasn’t aware of it until I started tinkering with them: MicroVMs are not on the lambda client. There’s a separate service model.
import boto3
# not boto3.client("lambda")
client = boto3.client("lambda-microvms", region_name="eu-west-1")
run_microvm is not in the usual Lambda client, your boto3 is probably not too old, you’re simply looking in the wrong place :D.
The MicroVM shell #
The detail that turns MicroVMs from “interesting primitive” into “I could actually build on this” is the ability to have a real, natively supported shell, and not something that kinda resembles it: you can launch the VM with the SHELL_INGRESS network connector, ask for a shell token, and open a WebSocket:
TOKEN=$(aws lambda-microvms create-microvm-shell-auth-token \
--microvm-identifier microvm-abc123 \
--expiration-in-minutes 55 \
--query 'authToken."X-aws-proxy-auth"' --output text)
websocat "wss://${ENDPOINT}/shell" \
-H "Sec-WebSocket-Protocol: lambda-microvms.authentication.${TOKEN}, lambda-microvms, lambda-microvms.port.8022"
In this way you get a real PTY, with its shell prompt, on a machine, the way an old sysadmin like me has always liked it, without baking sshd into the image, running reverse shells or distributing keys, and without a bastion host quietly adding to your bill while you forget it exists for years.
If you’ve ever built a remote execution backend before, you know that “how do I get a shell in there” is where the ugly and insecure code lives. I still remember having to deal with GitLab’s Fargate driver, which even today it gets a shell into an ECS task by baking an SSH daemon into the build image, handing it a public key through an environment variable, and requiring that you name the container ci-coordinator to identify it and inject the SSH key. In 2026. To run a command on a machine you already own. There’s an open request to use ECS Exec instead, but it is still a request.
Apart from my rant on things that still survive today, with MicroVMs all you need is an API call and a WebSocket.
Everything on the VM goes through that same endpoint over HTTPS, using X-aws-proxy-auth as a header and X-aws-proxy-port when you want something other than the default 8080, and none of it is reachable without a token, which is the right default even if it’s one you end up designing around.
Hermes Agent: two methods and you’re done #
Hermes has a TerminalEnvironmentProvider base class and a registry that plugins write into. Built-in names are reserved, so you can’t accidentally shadow docker; lambda_microvm is free to use, so I will use it.
The provider implementation is made mostly of metadata: a name, an availability check, a health probe, some setup instructions, and a factory. What surprised me is how little the environment object underneath it has to do.
The base class already handles a lot of things like session snapshots, tracking your working directory across cd calls, interrupt handling, output limits, timeouts, environment passthrough. The backend supplies two methods:
def _run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None) -> ProcessHandle: ...
def cleanup(self) -> None: ...
You can find something to use as a template already in the tree: the Daytona backend models a cloud sandbox that stops on cleanup and resumes on next use, which is suspend and resume with different verbs.
OpenClaw: same idea, one awkward constraint #
OpenClaw’s version is a registerSandboxBackend call exported from its public plugin SDK. Built-ins live in one map, plugin registrations go in another that takes precedence, and the backend id is a plain string.
Unlike Hermes, where your method just calls the WebSocket and returns the output, OpenClaw’s exec path wants an argv to spawn locally. The SSH backend, for example, returns an ssh user@host ... command line that OpenClaw then spawns.
So the MicroVM backend will have to ship its own little bridge binary, something OpenClaw can launch that opens the WebSocket, streams stdio both ways, and exits with the remote exit code.
It’s not hard, but there’s a security implication worth knowing: on Linux, everyone can read argv through /proc; this means that the auth token can’t go on the command line, better safe than sorry with agents.
An idea I have in mind is to write the token in a file with 0600 permissions (readable and writable only by the owner), put the path in argv, read the file and delete the file when the command finishes.
Mapping an agent session onto a MicroVM #
The lifecycle mapping is where I expect to spend the actual implementation time. A few decisions I’ve already made in the spec:
One VM per session, not per tool call, because every launch costs you a snapshot read, so a sandbox that boots once per command can make your bill go very wrong, very fast.
Both frameworks make this straightforward, because both already give a backend a stable identifier for the session, a task id in Hermes and a scope key in OpenClaw, so you tag the VM with it at launch and look it up again on the next call: only a genuinely dead one earns a fresh launch. Deriving the clientToken from the session key also means that if a launch call times out, the retry won’t spin up a second MicroVM that nobody ever uses.
Let the idle policy do the cleanup: we don’t have to implement custom code or timers, since a policy like the following will automatically suspend the VM after 15 idle minutes and deletes it after an hour that is being suspended.
{ "maxIdleDurationSeconds": 900, "suspendedDurationSeconds": 3600, "autoResumeEnabled": true }
Mind the eight-hour limit. The maximum duration is 28800 seconds; after this the service terminates the VM, so we need to implement a clear error and not simply say “socket exception” because we are not able to connect back to the environment.
And mind the token mismatch. Shell tokens cap at 60 minutes, but sessions can run up to 8 hours, so we’ll need a transparent refresh at around 80% of TTL that doesn’t kill an in-flight command. This thing is easy to forget, but annoying to debug, especially because it will appear once you think everything is running smoothly.
Lambda MicroVM caveats to know #
You have to know that there are a few limitations, some of which may not be an issue for you:
- ARM64 only. You should know it before building something for x86.
- It’s available in five regions (right now): us-east-1, us-east-2, us-west-2, eu-west-1, ap-northeast-1. No Milan, which stings a bit from here.
- Outbound UDP is blocked by default, and the localhost DNS stub doesn’t work for nested containers. If your sandbox image runs Docker inside, you will need to add
--dns 169.254.169.253or DNS queries will just fail for no apparent reason. - Images build from a Dockerfile in an S3 zip, not from ECR. The build environment has roughly 7.2 GB free, so pay attention to the image size.
- Every VM starts from the same memory. Anything generated while the image was being built is identical on every instance you launch from it, so ids and anything else that should be unique have to be generated when the VM starts, not when the image is baked.
- Nothing is reachable unauthenticated. Correct default, but it means no “just curl the health endpoint” while debugging.
None of these are real issues that will stop us, but knowing them before helps to avoid wasting time in troubleshooting.
About the money: MicroVMs vs AgentCore vs Fargate #
I ran the numbers before writing any code. Since the $58,000 week I read pricing pages the way other people read changelogs before upgrading something in production.
At us-east-1 ARM rates as I write this, 1 vCPU and 2 GB of memory works out to about $0.126 per running hour, against $0.108 for AgentCore’s Code Interpreter at the same shape and $0.040 for Fargate on ARM.
So MicroVMs are the most expensive option per hour. And it gets worse before it gets better: AgentCore only bills CPU you actually consume, with I/O wait free. For an agent that spends most of its life waiting on a model to think, that’s brutal on a like-for-like comparison.
The suspend policy closes most of that gap, since a suspended MicroVM costs storage and nothing else.
There’s something else we need to consider in this comparison that is not so obvious, and can change our economics: how you’re supposed to size the thing at all.
Size for the average, not for the peak #
MicroVMs are burstable, and the Lambda pricing page is telling us quite explicitly about what this means (just a note: if you follow the link, it will open on the Functions tab and the MicroVM numbers are in another tab):
During peak activity, your MicroVM can vertically scale up to 4x the baseline (up to 8GB / 4vCPU), with no action required on your part.
and, the part that we like:
When your workload consumes resources above the baseline, you are charged only for the active duration of the additional memory and vCPU consumed, not for the peak capacity.
This changes a habit most of us built over a decade of AWS serverless things: with a Lambda function you pick one memory number and pay it for every millisecond of every invocation, including the slow ones where you’re just waiting; with a Fargate task you pick a task size and pay it for the task’s entire life. In both cases you’re provisioning for the worst minute of the hour and being billed for it during the other fifty-nine; it kinda reminds me of a lighter version of the on-premises sizing strategy (with the great exception that with serverless you can start small and adapt the sizing as you go).
With MicroVMs you size the baseline for the average, and the expensive minute costs you the expensive minute, without paying a tax for the peak in advance.
Take a session that mostly sits at 1 vCPU and 2 GB, and spikes to the ceiling for ten percent of its running time (a dependency install, a test suite, one over-enthusiastic ffmpeg):
| Sizing approach | Cost per running hour |
|---|---|
| Baseline at 4 vCPU / 8 GB, held all hour | $0.504 |
| Baseline at 1 vCPU / 2 GB, bursting to 4x for 10% of the hour | $0.164 |
You can end up paying three times as much for capacity you only need for a fraction of the task.
For an agent workload this is close to ideal, because an agent’s load profile is spiky by nature: idle, idle, idle, then a full build. The old instinct is to provision for the build, but here you provision for the idle and let the build cost what it costs.
Mind that the 4x ceiling is still a ceiling, so if your genuine peak needs more than four times your average, you raise the baseline and pay for it.
So what are we paying for? #
I want to be honest about the thesis, because “it’s cheaper” would be a lie. You’re not saving money. You’re buying isolation, and the burst model just means you’re not overpaying for it while your tasks wait.
If you don’t need that isolation, use Fargate and save the money. If you do, now you know what it costs and how to size it.
Where to go next #
The design is done, the specs are written, and now comes the part where AWS tells me which of my assumptions were wrong. I won’t write those plugins alone: my notes and specs are going to Claude Code, and the plan is to let it take the first implementation pass on both repos while I review, argue, and measure.
There’s something pleasingly circular about using an agent to build the cage that the next agent is going to live in.
So part two is the build: two repos, real numbers, and at least one claim from this article that turns out to be wrong. My money’s on the token refresh.
Have you put an agent inside a MicroVM already? I’m interested in how you handle file sync, because pushing a workspace through a PTY feels wrong and I still have to think about it. Let me know in the comments, or find me on LinkedIn.