Proxylity

Now Available: Lambda MicroVM Destinations

By Lee Harding | November 8, 2026 | 11 min read
UDP clients send packets to Proxylity UDP Gateway, which starts one Lambda MicroVM per tenant. Two tenants are running with state in memory; a third is suspended.

UDP Gateway now supports AWS Lambda MicroVM Images as Destinations. This is an exciting new addition that unlocks significant new capabilities and performance improvements for stateful protocols.

Our existing Lambda integration is a workhorse on our platform. As with all destinations that support replies (Request/Response) we make a deal with your code: we hand it a batch, it returns some replies. The Lambda Event Function integration is limited by how Invoke works, and your code can't count on anything being remembered between one call and the next. It's a good model, and most of what teams build on UDP Gateway fits it. But not everything.

A game server has a world running as a stateful simulation shared by many players. A device fleet has partially-completed firmware transfers and a rate limiter per client. All of it is state, and until now the honest answer using Lambda Event Functions was to put it somewhere else (Valkey, DynamoDB, S3, Momento, etc.).

Today UDP Gateway can deliver packets to AWS Lambda MicroVMs and manage stateful MicroVM instances for you.

Lambda Event Function's promise

Lambda Event Functions reuse execution environments and create and destroy them as needed to serve the request rate. A global (static) variable set during one invocation is often still there during the next. Reuse is an optimization, not part of the contract. An environment can be reclaimed at any time, so a burst of traffic may create new ones with empty globals, and each environment handles one invocation at a time, so concurrency means more environments, each with its own private copy of whatever it caches. Code cannot treat a global as the source of truth.

With functions (including Step Functions) the state necessarily lives in an external store, and processing a batch means read the session, apply the packets, write it back. For many protocols and use cases that works fine. For others, less so. The state is hot, and touched by every packet, and pays the price of a round trip to the store. When two batches for the same session land on different environments at the same moment it becomes a concurrency problem: conditional writes, version counters, retries. It gets complicated and expensive.

Tenant isolation mode, which we added support for a year ago, tightened one half of this. An environment only ever serves one tenant, but there can still be multiple environments per tenant. The environment is still something Lambda can take away between invocations, and your code still has to behave as though it will.

What a MicroVM gives you instead

A Lambda MicroVM is a full Linux environment, built from a container image you provide, that keeps running and keeps its memory between requests (until it's suspended or terminated). Mechanically it's unremarkable, which is the appeal: your application is an ordinary long-running process that listens on a port and receives HTTP POSTs. State is just variables: A session table is a map; An index is an index. No state serialization boundary is required between one packet and the next.

But you don't get automatic durability. A MicroVM lives for at most eight hours, running and suspended time combined, and it can be terminated earlier than that. Treat its memory as a very fast cache of state you could rebuild, not as the only copy of anything you can't afford to lose. If you can't rebuild it, checkpoint it to a store on your own schedule. The difference from the function model is that now you choose when to pay for that write, instead of paying it on every batch or packet.

The destination is the MicroVM image ARN, not a running MicroVM. Which brings us to the configuration options.

Affinity is the gateway's job

If you've read our piece on stateful and stateless protocols, you'll remember sticky sessions: the load balancer sends a given client to the same server every time, and everyone hopes that server stays available. Stateful designs on server fleets have always needed some version of that, and it has always been where the operational pain accrued.

The same problem exists here, and we solve it in the gateway, with one important difference. Affinity is to an environment identified by a key, not to a server. You supply the key as a binary range expression evaluated against each packet:

Arguments:
  TenantIdExpression: "u8[0] >> 6 ?? 0"
  TenantIdFormatter: hex

This one takes the top two bits of the first byte, so there are at most four tenants, and ?? 0 sends an empty packet to tenant 0 instead of failing the batch. Deployments may also key on a device ID, a session ID, or a customer prefix. Same key, same MicroVM. Batches are split by tenant before delivery and tenants are delivered concurrently.

Choosing the key is the design decision in this feature, because it sets three things at once: the unit of state, the unit of concurrency, and the blast radius. Make it too coarse and you've built a singleton with extra steps. Make it too fine and you've made it unnecessarily expensive. If you're not sure, ask what you'd shard on if this were a database.

The gateway also owns the lifecycle that goes with the key. First packet for a key starts a MicroVM. A suspended one is resumed. A terminated one is replaced. From the client's side the address and port never change, and nobody outside the gateway needs to know a MicroVM exists. Your application sees a small, boring contract:

{
  "Messages": [{
    "Tag": "opaque-correlation-tag",
    "Remote": { "IpAddress": "203.0.113.10", "Port": 45678 },
    "Local": { "Domain": "ingress-1", "Port": 15678 },
    "Formatter": "base64",
    "Data": "aGVsbG8="
  }]
}

and replies come back matched by Tag:

{ "Replies": [{ "Tag": "opaque-correlation-tag", "Data": "SGVsbG8gZnJvbSBNaWNyb1ZNLi4u" }] }

That's the same packet format Lambda destinations use, so a handler you already understand ports over without ceremony. The example is a few dozen lines of Node.js that counts packets in a variable and tells you which MicroVM answered. The counter surviving in RAM from one batch to the next is the entire demo.

Isolation by routing

In a multi-tenant process, isolation is a key security concern. Every request's access, in every code path, must align with the customer. One missed rule, one rarely-run branch, and tenant A reads tenant B's data — a flaw that too often passes code review because each line looks correct on its own.

With a MicroVM per tenant, the access decision starts when the MicroVM is run. The optional run hook payload includes the TenantId. This happens before your packet handling code runs. Access is then limited to one tenant's packets, memory and data, so tenants can't be mixed up. That's the same property Lambda Event Function tenant isolation mode gives, and it's a worthy comparison: there the isolation is per environment and the environment is disposable, here in our MicroVM integration it's per environment and the environment is durable enough to hold a session. Isolation you get by construction is cheaper than what you get by "being careful".

Some boundaries to know about to avoid surprises:

The MicroVM lifecycle

Suspend and resume. A MicroVM that sees no packets for IdleDurationSeconds is suspended and resumes automatically on the next one. The default is 900 seconds; the range is 60 to 28,800, and the top of the range means never suspend. Memory survives the trip, which is the point, but the first packet after a resume pays a minor price in latency. Pick the idle time by asking how long a tenant can be quiet before a slower first reply matters to clients.

The first packet pays for the start. The MicroVM has to reach RUNNING before it can answer, so the packet that triggers a start may be dropped or answered late (a couple seconds or more). Test with that in mind rather than being surprised that your first ncat goes without an expected reply.

Starts converge, they aren't exclusive. If the first packets for a tenant reach several UDP Gateway nodes at once, each may start a MicroVM. One is used, and the others are terminated within seconds. That's a deliberate trade: coordinating a single start across nodes on the packet path would cost more than occasionally starting a spare. The consequence for you is that /run has to be safe to execute more than once per tenant. Don't register the tenant and instance in an external system from the hook without safeguards.

Snapshots

Lambda builds the image by running your application until its /ready hook returns, then snapshotting it. Every MicroVM starts from that snapshot. Anything you generated at startup (a random seed, a UUID, an ephemeral key) is therefore identical in every MicroVM. It's the familiar cloned-VM problem. So be careful and put per-instance values in the /run hook, which receives the MicroVM ID, the tenant, and the destination's extra data. The example app sets its MicroVM ID there for exactly this reason.

Failure handling. In the case of a failed delivery, UDP Gateway looks at the MicroVM's state. A terminated MicroVM is replaced, a suspended one is resumed, and a running one is assumed good (so errors your application data don't pathologically start new MicroVMs). If a MicroVM terminates within five minutes of starting (a failing /run hook is the usual cause) it isn't replaced for five minutes, and packets for that tenant are dropped in the meantime, with the reason written to the destination's log group. That back-off exists as a second defense to prevent a broken image from turning into a start loop.

Wiring it up

The image is declared with the AWS::Serverless::MicrovmImage SAM resource, and the destination points at its ARN. A trimmed version of the example:

UdpListener:
  Type: Custom::ProxylityUdpGatewayListener
  Properties:
    # ...
    Destinations:
      - Description: Deliver UDP packets to a Lambda MicroVM
        Role:
          Arn: !GetAtt DestinationRole.Arn
        DestinationArn: !GetAtt EchoMicrovmImage.ImageArn
        Arguments:
          IdleDurationSeconds: "900"
          TenantIdExpression: "u8[0] >> 6 ?? 0"
          TenantIdFormatter: hex
          ExecutionRoleArn: !GetAtt MicrovmExecutionRole.Arn
          RunHookExtraData: '{"Example":"lambda-microvm"}'

With an unversioned ARN, new MicroVMs run the latest active version of the image. Running ones aren't replaced when you publish, so a rollout is gradual by nature. Pin a version (...:microvm-image:my-image:3) if you want the destination to hold still. The destination role is assumed by Proxylity, and because MicroVM actions are authorized against the image, scoping them to the image's ARN limits the role to MicroVMs started from that image. The full policy is in the destination documentation and the example template.

When to choose MicroVMs

Start with a function. If the state can be rebuilt cheaply from the packet, or lives comfortably in a store you were going to have anyway, a Lambda destination is simpler and has no start-up story to explain. Reach for a MicroVM when the state is hot, per-key, and touched by nearly every packet, so that loading it per batch is most of your cost. Or when the software wants to be a long-running process, because it's an existing server, a native library with real startup cost, or something with background threads. Or when per-tenant isolation is a requirement you'd rather enforce by routing than by review.

Availability

MicroVM destinations are available today in UDP Gateway, in AWS Regions where Lambda MicroVMs are available.

A couple wrinkles on the tooling side:

For a quick start, deploy the Lambda MicroVM example and send a few packets with ncat. Each tenant's counter climbs in the replies, and picks up where it left off after a suspend. The Lambda MicroVM destination documentation covers every argument, the lifecycle, and the run hook payload.

Ready to modernize your UDP backends?

Get started with Proxylity UDP Gateway today. No upfront costs — pay only for what you use.

Buy with AWS Try the Examples Explore Documentation