PTEN

Lambda MicroVMs: shell access, lifecycle hooks and the best practices that matter

Lambda MicroVMs now takes a shell. How to connect, implement lifecycle hooks in Node, and apply the performance, security and cost best practices.

Author
Will Peixoto
Published
20 Oct 2026
Reading
11 min
Language
EN

🇧🇷 Leia em português: Lambda MicroVMs: shell na Lambda, lifecycle hooks e as best practices que importam

Let me tell you about a scene anyone from the infra side will recognize right away. When Lambda showed up back in 2014 and started spreading, infrastructure folks would come over and ask: "OK, but how do I connect to the Lambda to check it's running? Run a ps, a tail, look at the app from the inside?". And the answer was always the same: you don't. A function is a black box, you look at CloudWatch and pray.

Well, with Lambda MicroVMs you can now attach a real shell. =D In this post I show how to open that shell, how to implement in Node the lifecycle hooks that keep the MicroVM healthy, and the official best practices for performance, security and cost.

Connecting a shell to a Lambda MicroVM

There are two ways in. In the console, you select the MicroVM and click Connect. From the CLI, you generate a shell token and connect:

bash
aws lambda-microvms create-microvm-shell-auth-token \
  --microvm-identifier <id> --expiration-in-minutes 30

# once inside, list the tasks and exec into one:
ctr task ls
ctr task exec -t --exec-id shell <id> /bin/sh

Shell is opt-in. The MicroVM has to be launched with the SHELL_INGRESS network connector, and if it came up without it, create-microvm-shell-auth-token returns a ValidationException. Connectors are set on run-microvm and don't change while the MicroVM is running, so you decide at launch time which MicroVMs accept that way in.

For the rest of your debugging, three fields help a ton: terminationMessage on get-microvm (for a MicroVM that died), the build logs in CloudWatch under /aws/lambda-microvms/<image-name>, and StateReason when a network connector lands in FAILED.

Lifecycle hooks: what keeps the MicroVM standing

Remember the snapshot caveat (the MicroVM boots from a frozen state, so anything that needs to be fresh can't be frozen)? AWS handles it with lifecycle hooks: four HTTP endpoints your app implements, which the service calls with a POST at the right moment.

  • /run: post-initialization. This is where you generate UUIDs, secrets and random values, never at image build time, or every MicroVM is born with the same “random”. Use a CSPRNG. /run also receives in its body the runHookPayload (per-MicroVM config, such as a tenant ID, in a string of up to 16 KB that you pass with --run-hook-payload on run-microvm) and the microvmId, which Lambda injects.
  • /suspend: graceful cleanup before suspending (flush pending writes, close connections, release resources).
  • /resume: revalidates connections and cached state. AWS SDK connections usually recover on their own; custom connections may not, so check them here. The MicroVM stays SUSPENDED while this hook runs and only flips to RUNNING once it returns 200.
  • /terminate: flushes data before shutting down. It only runs on final termination; before a suspend, /suspend is the one doing the work.

The hooks arrive on the path /aws/lambda-microvms/runtime/v1/<hook>, on a port you configure, and external traffic only starts flowing after /run returns 200. If /run fails or times out, the MicroVM can go straight to TERMINATING without ever reaching RUNNING, so handle errors and timeouts inside the hook.

About the port: the docs don't tell you to split hooks and app, and in the example below both share 8080, the default port for external traffic. Make that call on purpose, looking at which ports the access token opens through allowedPorts.

There are also two hooks for the image build phase. /ready returns 200 once the app has finished starting, and that's when the service takes the disk and memory snapshot (while you're not ready, return 503 right away and Lambda keeps retrying until the timeout). /validate is called after the build, on a fresh MicroVM created from the image: that's where you run test payloads that exercise your code paths, and Lambda tracks which snapshot regions were touched so it can prefetch them and cut startup latency. So /ready makes sure the snapshot captures an app that's ready to serve, and /validate is the one that speeds up boot.

Sequence diagram of a MicroVM lifecycle: the /ready hook unlocks the snapshot, /validate runs on a MicroVM created from the image, then /run lets traffic in and the suspend, resume and terminate cycle happens at runtime
Fig. 01

In our Node HTTP server it looks like this:

js
const http = require("http");
const crypto = require("crypto");

let sessionSecret; // generated in /run, NEVER at image build time

function ok(res, body) {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}

const server = http.createServer((req, res) => {
  const HOOK = "/aws/lambda-microvms/runtime/v1";
  switch (req.url) {
    case `${HOOK}/run`:        // randomness and init live here; return 200 before traffic comes in
      sessionSecret = crypto.randomUUID();
      return ok(res, { ready: true });
    case `${HOOK}/suspend`:    // cleanup before freezing
      return ok(res, { cleanedUp: true });
    case `${HOOK}/resume`:     // revalidate connections/state on wake-up
      return ok(res, { revalidated: true });
    case `${HOOK}/terminate`:  // flush data on shutdown
      return ok(res, { flushed: true });
    default:
      // ... your actual application
      return ok(res, { hello: "world" });
  }
});

server.listen(8080, "0.0.0.0");

If the app stops answering after a resume, suspect number one is a missing /resume (or the app not binding to the right port, like 8080, after it wakes up).

Quick proof: the snapshot freezes your “random”

That warning about generating randomness in /run sounds like a detail until you measure it. I put together a small demo that runs locally, on my machine: no AWS, no cost, just the Node stdlib. It simulates the build producing the snapshot and boots two “MicroVMs” from it. The code ships with the demo post, the series finale.

The output, captured for real:

texto
== 3. antes do /run: o servico nao entrega trafego ==
amostra-A  GET /  -> HTTP 503

== 5. depois do /run 200: trafego liberado ==
amostra-A  GET /  -> HTTP 200

== 6. veredito ==
token gerado no BUILD  A vs B: IGUAL     <- colide entre analises
   A: 6fcf9c71-2024-4901-acfb-2593ed5a2fbb
   B: 6fcf9c71-2024-4901-acfb-2593ed5a2fbb
token gerado no /run   A vs B: diferente <- ok
   A: 2011db3c-3120-4b9a-b89d-caa48b034c8f
   B: 496f5bfc-7c7f-4cd7-b39f-2ea3c9b8c281

(The demo prints in Portuguese: “antes do /run” is before /run, “IGUAL” is identical, “diferente” is different.) You can see both things in the same output: the token baked at build time comes out identical in both, and traffic only gets a 200 after /run responds.

Heads up: this demo does not run on Lambda MicroVMs. It exercises the hooks contract locally, with a snapshot.json standing in for the real freeze. It's there to show the behavior. Proof on the real service only comes from running on a MicroVM in AWS, and that's for the demo post, the series finale.

Official best practices, by pillar

Performance. Pick the smallest MicroVM that covers your CPU, memory and bandwidth (bandwidth scales with configured memory). Keep the snapshot lean: strip build dependencies and temp files before the image is captured, which cuts run and resume time. And for latency-sensitive workloads, keep your most-used environments suspended so they resume fast.

Security. Issue short-lived tokens (15 to 30 min) and have refresh logic on the client before they expire. Scope the token to the ports the app actually uses, with the allowedPorts parameter. Sensitive traffic (database, internal API) goes through a VPC egress connector, which goes through your security groups and network ACLs. And apply least-privilege: split the build role (image creation) from the execution role (runtime), each with the bare minimum. Handy detail: /ready and /validate run under the build role, and the runtime hooks run under the execution role.

Cost. Set maxIdleDurationSeconds in the idle policy to suspend idle MicroVMs (a suspended MicroVM pays no compute, only snapshot storage). Put a safety ceiling with the maximum duration (--maximum-duration-in-seconds in the CLI, up to 8 hours, counting running plus suspended time) so a forgotten instance doesn't surprise you on the bill. Use suspendedDurationSeconds to automatically terminate a MicroVM that sat parked for too long. Tag your images for cost allocation in Cost Explorer, and delete image versions you no longer use, because they cost storage too.

Before you decide how much isolation and availability your use case needs, it's worth looking at the price of each step up. I ran those numbers in High availability has a price: resilience is a decision, not a stack.

Browser apps: a proxy in front of the MicroVM

An app that runs in the browser (a VS Code server, a web IDE) runs into a rule in the docs: every request to the MicroVM endpoint needs the JWE token in the X-aws-proxy-auth header, and there's no unauthenticated option. When the user opens a URL, clicks a link or loads an iframe, the browser won't let you set that header. For a WebSocket opened by your own JavaScript, the docs let you pass token and port as subprotocols (lambda-microvms.authentication.<token>), but that doesn't cover regular navigation.

The architecture answer is a reverse proxy of your own in front (the Gateway pattern): a component that takes the browser request already authenticated the way your app does it, fetches or mints the MicroVM token, and injects X-aws-proxy-auth before forwarding. It also needs a user → MicroVM map, because each MicroVM has its own endpoint and there's no load balancing across them. With that map, when the user closes the tab and comes back later, you know which instance to route to. The orchestrator that already calls run-microvm is a natural place to keep that map.

If all you need is a custom domain, AWS published a pattern with an Application Load Balancer rewriting the Host header, but there the client still has to send the token. It doesn't replace the auth proxy.

If what's orchestrating these MicroVMs is an AI agent operating your account, the other side of the coin is permission control, which I covered in AWS MCP Server: which one to use, when, and how to set it up.

The errors you'll see (and what to do)

From the troubleshooting table in the docs, these are the ones I'd keep at hand:

  • 502 Bad Gateway: the app crashed or isn't listening. Check the EXPOSE and CMD in your Dockerfile and the runtime logs. With auto-resume on, it can also be a MicroVM that failed to resume (check its state with get-microvm).
  • 429 Too Many Requests: you went over the rate (account or MicroVM). Retry with exponential backoff and jitter.
  • Dropping connections: idle timeout. Implement keepalive (ping/pong) or raise maxIdleDurationSeconds.
  • Build failed (CREATION_FAILED): look at the logs under /aws/lambda-microvms/<image-name>, check your Dockerfile syntax and the build role's S3 permissions. Run docker build locally to reproduce.

And for basic monitoring: when a MicroVM terminates out of nowhere, get-microvm returns a stateReason with the cause. That's the first place to look.

What you take away

  • You can shell into Lambda now, via the console or create-microvm-shell-auth-token, but only if the MicroVM was launched with the SHELL_INGRESS connector.
  • The lifecycle hooks (/run, /suspend, /resume, /terminate) are what keep the MicroVM healthy. Generate randomness in /run, revalidate connections in /resume, and use /validate to shorten boot time.
  • Security: short-lived tokens with refresh, per-port scope, VPC egress, and a build role separate from the execution role.
  • Cost: suspend when idle, cap the duration, terminate what sat parked, and clean up image versions you don't use.

Sources

Running a MicroVM use case already? Tell me which hook saved you (or burned you for not having it). And if you're from the infra side and always wanted to shell into a Lambda, now's your moment. If this post helped, drop a like and share it around. Thanks a lot!

BUILD. SCALE. REPEAT. =D

Will Peixoto

Will Peixoto is a Staff Engineer at Serverless Guru and writes about serverless, AWS architecture and AI agents: what he'd pick, why, and what it costs.

More from the series

New posts, straight to your inbox

One serverless, architecture or AI agents topic on AWS per issue: what I'd pick, why, and what it costs. No spam.

Opens the subscription page in a new tab.