Achieving CNIL/EU ePrivacy compliance for email tracking with Amazon SES

Post Syndicated from Toni Pivcevic original https://aws.amazon.com/blogs/messaging-and-targeting/achieving-cnil-eu-eprivacy-compliance-for-email-tracking-with-amazon-ses/

Open and click tracking have long been foundational email metrics. Multiple data protection authorities, including in EU and Canada, have issued guidance requiring explicit opt-in consent before deploying open tracking pixels or click link wrapping in email. As privacy expectations evolve, a growing number of jurisdictions now require senders to obtain explicit consent before tracking whether a recipient opened an email or clicked a link. In this post, you learn how to use Amazon Simple Email Service (Amazon SES) configuration set overrides to control open and click tracking per request.

How Amazon SES tracking works

Amazon SES enables open and click tracking only when explicitly configured by you. Amazon SES does not inject tracking pixels or wrap links by default.

Open tracking — When you add an event destination publishing OPEN events to a configuration set, Amazon SES inserts a 1×1 tracking pixel (served from awstrack.me) into HTML email sent using that configuration set. When a recipient opens the email and their client loads images, Amazon SES records the open event and publishes it to the configured destination.

Click tracking — When you include CLICK events in a configuration set’s event destination, Amazon SES rewrites links in HTML email to redirect through awstrack.me, recording click events before sending the user to the original URL.

Configuration sets — Configuration sets are the control surface for both features. Tracking activates only when an event destination explicitly includes OPEN or CLICK in its matching event types. You can have multiple configuration sets with different tracking configurations and select the appropriate one at send time.

Per-request tracking overrides — Amazon SES now supports open and click tracking override parameters directly in the SendEmail and SendBulkEmail APIs through the ConfigurationOverrides object. You can enable or disable open tracking and click tracking on an individual API call, without maintaining separate configuration sets. The override takes precedence over the tracking behavior defined in the associated configuration set, giving you fine-grained, per-recipient control at send time. This is the most direct way to honor recipient-level consent choices.

Event publishing and metrics — Open and click events are published to the destinations you configure: Amazon CloudWatch, Amazon Data Firehose, or Amazon EventBridge. Plan for reduced fidelity in open-rate dashboards for recipients in jurisdictions where you cannot track without consent.

Note on Amazon SES Contact Lists: Contact Lists manage topic-level subscription preferences (for example, marketing versus transactional) and control whether Amazon SES delivers to a contact. They do not control tracking behavior. There is no native mapping between a contact’s subscription status and tracking pixel injection. Tracking consent must be managed separately in your application.

Prerequisites

To follow the steps in this post, you should be familiar with Amazon SES configuration sets and basic email sending concepts. You also need the following:

  • An AWS account with Amazon SES out of sandbox mode.
  • AWS Identity and Access Management (IAM) permissions to create and manage Amazon SES configuration sets (ses:CreateConfigurationSet, ses:CreateConfigurationSetEventDestination, ses:DeleteConfigurationSet).
  • The AWS Command Line Interface (AWS CLI) version 2 installed and configured, or access to the AWS SDK for Python (Boto3).

The most direct approach to consent-based tracking uses the ConfigurationOverrides parameter in the SendEmail API. This approach requires only a single configuration set with tracking-enabled event destinations. At send time, you override the tracking behavior based on each recipient’s consent status.

AWS SDK:

import boto3

ses_client = boto3.client("sesv2", region_name="us-east-1")

def send_campaign_email(subscriber: dict, subject: str, html_body: str):
    # Determine tracking behavior based on recipient consent
    tracking_consent = subscriber.get("tracking_consent", False)

    # Use ConfigurationOverrides.Tracking to control per-request behavior
    ses_client.send_email(
        FromEmailAddress="[email protected]",
        Destination={"ToAddresses": [subscriber["email"]]},
        Content={
            "Simple": {
                "Subject": {"Data": subject},
                "Body": {"Html": {"Data": html_body}},
            }
        },
        ConfigurationSetName="my-config-set",
        ConfigurationOverrides={
            "Tracking": {
                "OpenTrackingEnabled": tracking_consent,
                "ClickTrackingEnabled": tracking_consent,
            }
        },
    )

The ConfigurationOverrides.Tracking object takes precedence over the configuration set’s event destination settings for that individual send. If the recipient has not consented, Amazon SES does not inject the tracking pixel or wrap links, regardless of whether the configuration set has OPEN and CLICK events enabled.

Advantages of per-request overrides:

  • No need to maintain separate configuration sets for tracked versus untracked sends.
  • A single configuration set can handle all recipients, which simplifies event destination management, suppression, and DomainKeys Identified Mail (DKIM) and domain settings.
  • Per-recipient control without branching logic for configuration set selection.
  • Works with SendBulkEmail as well, setting tracking overrides per recipient in the bulk request.

SMTP interface — If you send through SMTP, per-request overrides are not available. Use Option B (separate configuration sets) instead.

Option B: Use separate configuration sets

If you send through SMTP or prefer to separate tracking behavior at the configuration set level, create two configuration sets: one with tracking enabled for consented recipients, and one with no tracking for non-consented recipients.

Console: In the Amazon SES console, choose Configuration sets, and then choose Create configuration set. To enable tracking on the first set, add an event destination and include OPEN and CLICK in the matching event types. Create a second set with no OPEN or CLICK event destinations, or omit event destinations entirely.

AWS CLI:

# Configuration set with tracking enabled (for consented recipients)
aws sesv2 create-configuration-set \
    --configuration-set-name tracking-enabled

aws sesv2 create-configuration-set-event-destination \
    --configuration-set-name tracking-enabled \
    --event-destination-name open-click-destination \
    --event-destination '{
    "Enabled": true,
    "MatchingEventTypes": ["SEND","DELIVERY","BOUNCE","COMPLAINT","OPEN","CLICK"],
    "CloudWatchDestination": { ... }
}'

# Configuration set with no tracking (for non-consented recipients)
aws sesv2 create-configuration-set \
    --configuration-set-name no-tracking

Replace { … } with your CloudWatch destination configuration. For the full parameter structure, see Managing Amazon SES event destinations.

No event destination for OPEN or CLICK means no pixel is injected and no links are wrapped.

Send-time selection:

Your application selects the configuration set for each message based on the recipient’s consent status.

import boto3

ses_client = boto3.client("sesv2", region_name="us-east-1")

def send_campaign_email(subscriber: dict, subject: str, html_body: str):
    config_set = (
        "tracking-enabled"
        if subscriber.get("tracking_consent")
        else "no-tracking"
    )
    ses_client.send_email(
        FromEmailAddress="[email protected]",
        Destination={"ToAddresses": [subscriber["email"]]},
        Content={
            "Simple": {
                "Subject": {"Data": subject},
                "Body": {"Html": {"Data": html_body}},
            }
        },
        ConfigurationSetName=config_set,
    )

SMTP interface — Set the configuration set through the X-SES-CONFIGURATION-SET header:

X-SES-CONFIGURATION-SET: no-tracking

To disable click tracking on individual links within a tracked email (for example, your unsubscribe link), use the ses:no-track attribute:

<a ses:no-track href="https://anycompany.example.com/unsubscribe">Unsubscribe</a>

Amazon SES strips the ses:no-track attribute before delivery, so recipients never see it. This works with both Option A and Option B.

Regardless of which option you choose, Amazon SES provides the mechanisms to enable or disable tracking at send time. You are responsible for:

  • Maintaining a consent database recording each recipient’s tracking consent status.
  • Determining at send time whether a recipient has consented to open and click tracking.
  • Passing the correct override parameter (Option A) or selecting the appropriate configuration set (Option B) based on that determination.

Important: Once an email is delivered with a tracking pixel, it cannot be retroactively deactivated.

Capture consent at sign-up. France’s data protection authority (CNIL) recommends collecting tracking consent at the point of email address collection. Include a clearly labeled, unchecked checkbox. For example: “I agree to allow AnyCompany to track whether I open or click email to improve future communications.” This is a separate checkbox from consent to receive marketing email.

Store consent with proof. You must be able to demonstrate valid consent for each individual. Store a consent timestamp and source alongside the subscriber record:

import boto3
from datetime import datetime, timezone

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Subscribers")

def save_subscriber(email: str, marketing_consent: bool, tracking_consent: bool):
    table.put_item(
        Item={
            "email": email,
            "marketing_consent": marketing_consent,
            "tracking_consent": tracking_consent,
            "tracking_consent_timestamp": datetime.now(timezone.utc).isoformat(),
            "tracking_consent_source": "sign-up-form-v2",
        }
    )

Make refusal as straightforward as acceptance. Do not use pre-checked boxes or dark patterns that make opting out harder than opting in.

Provide withdrawal at any time. Every email sent with tracking enabled must include a link that recipients can use to withdraw tracking consent independently of unsubscribing. A “Manage email preferences” link in the footer, separate from the unsubscribe link, satisfies this requirement:

<p style="font-size:12px; color:#666;">
  <a href="https://anycompany.example.com/email-preferences?token={SUBSCRIBER_TOKEN}">Manage email preferences</a>
  &nbsp;|&nbsp;
  <a href="{UNSUBSCRIBE_URL}">Unsubscribe</a>
</p>

Replace {SUBSCRIBER_TOKEN} with a signed token that identifies the subscriber’s record in your consent database. Replace {UNSUBSCRIBE_URL} with your Amazon SES one-click unsubscribe URL or list-unsubscribe endpoint.

Monitoring and governance

Audit event volumes. Use Amazon SES event publishing to monitor open and click event counts. If you use Option B, compare volumes between tracking-enabled and no-tracking. If you use Option A, monitor the ratio of sends with tracking enabled versus disabled. A sudden drop in consented opens might indicate an issue with your consent capture flow.

Review data retention. If you rely on the deliverability-only exemption for any segment, verify that your data pipeline retains only the date of last open, not the time, IP address, or user-agent string. Collecting those fields, even temporarily, voids the exemption per CNIL guidance.

Document your consent architecture. Maintain a record of how and when each subscriber provided consent, which form version was in use, and where the consent data is stored.

Clean up

If you created test configuration sets while following this post and do not intend to use them, delete them to avoid unintended configuration being applied to future sends.

aws sesv2 delete-configuration-set --configuration-set-name tracking-enabled
aws sesv2 delete-configuration-set --configuration-set-name no-tracking

Conclusion

Amazon SES gives you the tools to obtain prior consent before tracking opens and clicks. With per-request tracking overrides, you can honor consent decisions inline with each API call, with no additional configuration sets required. For senders using SMTP, separate configuration sets achieve the same outcome. Combined with consent capture at sign-up and a preferences management link in every tracked email, you can maintain meaningful analytics for consented recipients while respecting the privacy of those who have not opted in.

For more information, see the following resources in the Amazon SES Developer Guide:


About the authors

ReadyOn’s Four Walls of tenant isolation on Amazon EKS

Post Syndicated from Roshan Daneshvaran original https://aws.amazon.com/blogs/architecture/readyons-four-walls-of-tenant-isolation-on-amazon-eks/

ReadyOn, an AWS Partner delivering an intelligent Labor Orchestration Platform for Fortune 100 enterprises, runs a multi-tenant platform on Amazon Elastic Kubernetes Service (Amazon EKS) that handles some of the most sensitive data in enterprise IT. Kubernetes is central to that platform, and it is also one of the most complex surfaces to secure. A default Kubernetes deployment is not secure out of the box: upstream Kubernetes historically lets anonymous requests reach the API server, where they are denied only by RBAC, pods run as root by default, and network policies do not exist until someone writes them.

For single-tenant clusters, hardening is a well-documented challenge. But when you add multi-tenancy, the threat model fundamentally changes. The concern shifts from whether an unauthorized user can reach the cluster to whether Tenant A can access Tenant B’s data. Most multi-tenant Kubernetes platforms rely on a single isolation mechanism: the namespace. Kubernetes designers never intended namespaces to be a security boundary.

ReadyOn’s Harmony platform powers workforce intelligence for Fortune 100 enterprises. It processes payroll data, organizational hierarchies, and operational analytics spanning hundreds of thousands of employees. Cross-tenant access would trigger regulatory obligations across multiple jurisdictions and damage the trust enterprise clients place in their technology partners. The stakes demanded something better than one wall.

This post describes the architecture of ReadyOn’s four independent layers of isolation on Amazon EKS. Their Four Walls model combines Kubernetes namespaces, Karpenter-managed dedicated node pools, Amazon Virtual Private Cloud (Amazon VPC) security groups, and per-tenant Amazon Aurora databases. Together they create a compound defense: to cross a tenant boundary, an unauthorized user would need to simultaneously overcome the Kubernetes API, the node scheduler, the AWS software-defined network, and the data layer.

The Four Walls model

ReadyOn’s architecture places four independent barriers between tenants, each operating at a different layer of the stack and requiring a fundamentally different technique to cross:

  • Wall 1 – Namespace isolation: Each tenant operates in a dedicated Kubernetes namespace with strict RBAC policies, resource quotas, and admission control. An Argo CD ApplicationSet generates these resources, so every tenant receives an identical security posture by construction.
  • Wall 2 – Compute isolation: Karpenter provisions dedicated, auto scaling node pools for each tenant using a dual-taint strategy. Every node carries both a tenant-identifying taint and a workload-type taint. A pod must tolerate both to be scheduled. This configuration prevents cross-tenant pod placement.
  • Wall 3 – Network isolation: Per-tenant Amazon VPC security groups restrict database access to only that tenant’s node pool. Kubernetes network policies enforce default-deny between namespaces. The cluster API server is private, accessible only through VPN with OpenID Connect (OIDC) and MFA.
  • Wall 4 – Data isolation: Each tenant has a dedicated Amazon Aurora database cluster and tenant-scoped secrets in AWS Secrets Manager. Workloads use short-lived credentials through IAM Roles for Service Accounts (IRSA), and each tenant has per-tenant observability instances. There is no shared database with row-level filtering.

The power of this model is layering. An unauthorized user would need to cross all four walls simultaneously to reach the tenant boundary. These are layered, overlapping controls: an unauthorized user must defeat them in combination, not one at a time. Because some layers share a control plane (admission control, GitOps, the EKS control plane, and IAM), ReadyOn treats them as defense in depth rather than as fully independent probabilities.

Wall 1: Namespace isolation on Amazon EKS

The namespace is the most visible tenant boundary in Kubernetes. While ReadyOn is realistic about the limitations of namespace isolation as a security mechanism, the namespace still forms the logical foundation upon which the other isolation layers build.

GitOps-enforced consistency

Every tenant namespace is provisioned through an Argo CD ApplicationSet. When ReadyOn onboards a new tenant, they add a single entry to a configuration manifest in Git. The automation generates all required resources: the namespace, network policies, RBAC bindings, resource quotas, and secrets configurations. There are no “legacy” tenants with weaker policies. Every tenant receives an identical security posture by construction.

RBAC and admission control

Each namespace carries role bindings that restrict API access to only that tenant’s resources. No tenant principal can list, get, or modify resources in another namespace. An admission control framework validates all resources against a policy set that enforces security contexts, blocks privileged configurations, and prevents creation of resource types that tenants should not own (DaemonSets, ClusterRoles, admission webhooks).

The generated RoleBinding scopes a tenant group to its own namespace only. Argo CD renders this from the tenant entry in Git, so every namespace gets the identical binding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-a-developers
  namespace: tenant-a
subjects:
- kind: Group
  name: tenant-a-developers
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: tenant-namespace-editor
  apiGroup: rbac.authorization.k8s.io

Self-healing drift correction

The GitOps controller continuously reconciles live cluster state against the declared state in Git. Any manual modification (whether from a misconfiguration or an unauthorized user who gains limited API access) is automatically reverted within seconds. A human operator never runs kubectl apply against a production cluster.

Wall 2: Compute isolation with Karpenter

Namespace isolation operates at the Kubernetes API layer. Wall 2 drops to the compute layer: the actual machines where tenant code executes. In Harmony, tenants do not share compute nodes.

The dual-taint strategy

ReadyOn’s Karpenter provisioners create nodes with two taints: a tenant-identifier taint (for example, tenant=acme-corp) and a workload-type taint (for example, workload=frontend). A pod must tolerate both taints to be scheduled. Tenant A’s frontend nodes are distinct from Tenant A’s batch nodes, and both are entirely separate from Tenant B’s infrastructure.

Admission controller validation

Even if a pod is created with forged tolerations, an admission controller validates that the toleration claims match the namespace’s tenant identity. The scheduler cannot be directed into cross-tenant placement.

Scoped node credentials

All application nodes enforce IMDSv2 with a reduced hop limit, preventing pods from reaching the instance metadata endpoint. The IAM role attached to each node is scoped to a single tenant’s resources, limiting the scope of a container escape. Platform nodes run on managed node groups with a separate taint that no tenant workload can tolerate.

Wall 3: Network isolation at the VPC layer

Walls 1 and 2 operate within the Kubernetes abstraction. Wall 3 drops below Kubernetes entirely, enforcing isolation at the Amazon VPC and security group level: infrastructure that the Kubernetes control plane does not manage and that a pod with limited access cannot manipulate.

Per-tenant security groups

Each tenant’s Amazon Aurora cluster is protected by a security group that allows inbound connections only from the security group attached to that tenant’s application nodes. Tenant A’s nodes cannot open a TCP connection to Tenant B’s database. This is an Amazon VPC security group rule enforced by the AWS software-defined network. Crossing it would require overcoming that networking layer itself.

Multi-tier VPC architecture

The VPC is divided into four tiers: a public perimeter tier (load balancers only), an application tier (EKS worker nodes in private subnets), a database tier (Aurora clusters with no internet access), and a control plane tier (private EKS API endpoint, VPN-only access with OIDC and MFA).

Default-deny network policies

Kubernetes network policies enforce default-deny for inter-namespace traffic. Tenant pods communicate only with pods in their own namespace and explicitly allowed platform services. All other traffic is denied and logged. Amazon VPC Flow Logs capture all network traffic for anomaly detection.

Wall 4: Data isolation with Amazon Aurora

The final wall addresses the ultimate target: the data itself. Walls 1 through 3 prevent an unauthorized user from reaching another tenant’s data. Wall 4 maintains data isolation even if all other layers fail.

The case against shared databases

A shared database places the entire burden of isolation on application-layer query logic, where a single missing WHERE tenant_id = ? clause can result in inadvertent disclosure. ReadyOn chose dedicated Amazon Aurora clusters per tenant. There is no row-level filter to forget. Tenant A’s code cannot construct a connection to Tenant B’s database: it lacks the endpoint, the credentials, and the network path.

Dedicated Aurora clusters also support unique AWS Key Management Service (AWS KMS) encryption keys per tenant. Each tenant’s data-at-rest is encrypted with a distinct KMS key, meaning that even if raw storage were inadvertently accessed, one tenant’s key cannot decrypt another tenant’s data. Equivalent per-tenant key separation is complex to achieve in a shared database, where every tenant’s rows share the same storage and separation depends on correctly implemented row-level security.

IRSA: No long-lived workload credentials

Every workload authenticates to AWS APIs by using short-lived credentials from AWS Security Token Service (AWS STS), issued through OIDC federation between the EKS cluster and IAM. Each tenant’s workloads assume a unique IAM role scoped to only that tenant’s resources. Credentials expire within minutes. Application workloads hold no long-lived access keys.

Per-tenant observability

Each tenant’s metrics, logs, and traces are routed to dedicated observability backends through OpenTelemetry. Even a spike in error rates (which could reveal sensitive operational intelligence) is invisible to other tenants.

Breaking the threat sequence at every stage

ReadyOn maps their defenses to their own 10-stage multi-tenant threat model, with each stage mapped to the relevant MITRE ATT&CK techniques. The critical inflection point is their lateral movement stage (Stage 8), where an unauthorized user in a Tenant A pod with limited access attempts to cross the boundary to Tenant B’s resources. Figure 1 illustrates this inflection point, showing an unauthorized process inside a Tenant A pod attempting to reach Tenant B and being stopped at each of the four walls.

Diagram of a Tenant A pod on the left and Tenant B resources on the right, separated by four numbered walls; six cross-tenant attack paths each stop at the wall that blocks them, and none reach Tenant B

Figure 1: Stage 8 lateral movement, where each of the six cross-tenant paths a compromised Tenant A workload might attempt terminates at the numbered wall that blocks it

Figure 1 depicts a Tenant A pod on the left and Tenant B’s resources on the right. Four labeled vertical barriers sit between them, numbered 1 through 4, representing the four walls in order: (1) namespace isolation, (2) compute isolation, (3) network isolation, and (4) data isolation. Each of the six cross-tenant paths an unauthorized user might attempt is drawn as an arrow from Tenant A that terminates at the numbered wall that blocks it: creating a route into another namespace stops at wall 1. Scheduling a pod on another tenant’s nodes stops at wall 2. Opening a database connection and sending cross-namespace traffic stop at wall 3. And retrieving secrets and querying monitoring data stop at wall 4. No arrow reaches Tenant B.

At Stage 8, all four walls apply simultaneously. ReadyOn systematically validates that every cross-tenant path is blocked:

  • Scheduling workloads on another tenant’s nodes is blocked by the dual-taint strategy plus admission controller validation.
  • Reaching another tenant’s database is blocked by per-tenant security groups at the Amazon VPC layer.
  • Reading another tenant’s secrets is blocked by IRSA scoping plus tenant-scoped secrets paths.
  • Querying another tenant’s monitoring data is blocked by dedicated per-tenant observability instances.
  • Communicating with pods in another namespace is blocked by default-deny network policies.
  • Creating a route that directs traffic into another tenant’s namespace is blocked by policy: tenants cannot create or modify ingress resources, and the platform ingress layer re-validates tenant context on every request.

ReadyOn validates these boundaries through regular adversarial exercises that simulate a tenant pod with full access and attempt every crossing path. In these exercises to date, no test has produced a successful cross-tenant path.

Workload hardening: The innermost defense

Every container in Harmony runs with Pod Security Standards at the “restricted” level:

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: [ALL]
  seccompProfile:
    type: RuntimeDefault

Containers cannot install tools, modify binaries, or use the kernel interfaces that container escape techniques depend on. Combined with IMDSv2 restrictions and IRSA, even a container escape does not yield useful node-level credentials.

GitOps as the security control plane

Every change to cluster state begins as a pull request requiring review and automated policy checks. The Git repository is the single source of truth. The cluster is a reflection of Git. Zero secrets are stored in Git: the External Secrets Operator resolves references to AWS Secrets Manager at deployment time. Even unauthorized access to the Git repository yields zero usable credentials.

Benefits

ReadyOn’s Four Walls architecture delivers measurable security and operational benefits:

  • 10 threat model stages defended, each mapped to MITRE ATT&CK techniques, with all six cross-tenant paths blocked at the lateral movement stage.
  • No long-lived credentials in application workloads: all workload authentication uses short-lived OIDC-federated tokens.
  • Consistent security posture across all tenants enforced by GitOps templating, with no legacy exceptions.
  • Self-healing drift correction reverts unauthorized cluster changes within seconds.
  • Multi-region active-passive architecture with automated failover through Amazon Route 53.

Conclusion

Multi-tenant Kubernetes security is not a single problem with a single solution. Namespace isolation is a strong logical boundary, and the Kubernetes documentation recommends pairing it with additional layers rather than relying on it alone as a security boundary. ReadyOn built those additional layers.

ReadyOn’s Four Walls model demonstrates that defense-in-depth is achievable on AWS by layering native services at independent abstraction layers: Kubernetes RBAC at the API layer, Karpenter at the scheduler layer, Amazon VPC security groups at the network layer, and IAM at the data access layer. Each wall independently raises the cost of a cross-tenant attempt, and together they provide defense in depth: an unauthorized user must defeat the walls in combination, not one at a time.

For organizations running sensitive multi-tenant workloads on Amazon EKS, the Four Walls model provides a reference architecture for zero-trust tenant isolation without sacrificing the economics and operational velocity that multi-tenancy delivers.

Learn more


About the authors

Saving another 100TB of RAM with math (and Rust)

Post Syndicated from Kevin Guthrie original https://blog.cloudflare.com/saving-100-tb-of-ram-with-math/

Cloudflare operates at a scale so big that even after working here for years, it doesn’t seem real. We have thousands of servers all over the world with petabytes of RAM and millions of CPU cores, and all of it is pushed to the max. As vast as those resources feel, they are still finite, and when you need every service to run on every node, it doesn’t leave room for wasted space.

At this scale, small improvements are greatly magnified, so even 1%-at-a-time improvements are worth celebrating. And some tweaks add up to a lot more: in this post, we’ll look at how small changes to a single algorithm reduced the memory footprint of one of our Pingora-based services significantly. That allowed us to reclaim more than 100TB of RAM globally, on top of the 100TB of memory the DNS team was able to shed last month.

Waste not

Maintaining equitable resource sharing between teams is not easy, especially in large organizations. One of the ways Cloudflare ensures the balance is kept is through the tireless efforts of the wonderful Performance team. 

This story starts with a ticket filed by Ivan who found: Excessive memory usage from pingora-ketama in Pingora Backend Router. The finding was that our internal load-balancing service, Pingora Backend Router (yes, PBR), was using significantly more memory than expected — specifically in structures associated with pingora-ketama, which is our open-source library for handling consistent hashing.

In order to talk about how we addressed this seeming overuse of memory, we need to talk about what consistent hashing even is, why we are using it in PBR, and how it became so memory hungry. Along the way, we’ll learn some Rust and even a little math.

Consistent hashing

Consistent hashing is a widely used method for distributing tasks across multiple servers in a way that does not require large changes when servers are added or removed. Internally we use it to route cacheable requests to servers by URL. This allows us to keep only one copy of a file stored per data center and gives a stable way to find the location of each file. We have mentioned this system before, but let’s take the time to walk through how and why this algorithm is used and how it works.

The key concept of consistent hashing is that while hash functions can accept any kind of input, their output is limited to a single unsigned integer (32, 64, or 128-bit integers depending on which hash function). This allows us to relate tasks and servers to each other in a consistent way. Most discussions of consistent hashing have you think of that output space as a continuous, circular ring that wraps around from its max value to zero. This depiction makes for some nice visualizations, but it can also make the simple concept of integer ranges seem more complicated than it needs to be. For our discussion, we’ll represent the 32-bit output of our hash function as a number line.

Now, let’s say we have a set of servers, A, B, & C, and a set of tasks t-z. We can map each onto the number line based on the hash of their representative values, so something like IP addresses for servers and cache keys for tasks.

Assigning tasks to servers is now just a matter of finding the first server to the left of each task. We can represent this visually by coloring in the region of hashes that will be associated with each server. Notice that the range covered by server C wraps around to the beginning, hence the idea that hashes exist in a ring.

And that’s it. At a base level, consistent hashing is this simple — but it doesn’t take long to see that there is room for improvement. Notice that the range covered by server A in our example is significantly larger than that of either B or C. This is a problem because the fraction of the requests a server handles is going to be proportional to the size of its range on the number line. Ideally we would like to guarantee each server will have an equal size, but because hashes are essentially random numbers, we have to talk about the size of the regions in terms of statistics. 😨

Math and consequences

First: don’t panic. I promise I'm not about to lie to you and that we will stay safely within the bounds of a day-one probability lesson. When we talk about statistical distributions, there are two big factors that help us quantify uncertainty in helpful ways: expected value and standard deviation. In (over-)simplified terms, expected value gives us a point where measurements based on a distribution will be centered, and standard deviation tells how close to that central point most measurements are likely to be.

For consistent hashing, we can calculate these factors for the fractional size of the range associated with one of N servers. (Details on where this formula comes from later).

In terms of concrete numbers, let’s say we have 100 servers. The formulas above give:

That tells us that we can expect that the range each server handles will be centered around 0.99% of the total and most of the lengths to fall within 1% of what's expected. This sounds good until we realize that that’s 0.99% of the total length. We need to scale the standard deviation by the expected value to see how big the error is as a fraction of the target size. This value is called the coefficient of variation.

What if we add hashes?

The simplicity of consistent hashing is a double-edged sword. It’s easy to understand and implement because everything is turned into easily-relatable hashes on the same numberline, but any improvements to the system will also need to be relatable to that numberline. That means the solution to any consistent hashing problem can only be more hashes. It’s less like a golden hammer (a tool with which all problems look like nails) and more like a golden nail in that it turns all tools into hammers.

To solve the problem of imbalanced workloads, we can add multiple hashes to represent each server instead of just one. We’ll get to the math behind this momentarily, but it should make some intuitive sense that while each individual range has a large standard deviation, adding a bunch together should make their total size even out. If we take our three-server example from the above diagrams and add two more hashes at random for each server, we see that it helps even out each server’s workload. 

This is an admittedly contrived example. The random nature of the system means there’s no guarantee how much improvement you will get from adding 2 additional hashes per server, but it should make some intuitive sense that combining more of these hash segments together produces a more even distribution. Each segment in the sum has a chance of balancing another. Maybe one is too short; maybe one is too long. This is essentially what the law of large numbers tells us should happen… The obvious problem is it only works for large numbers. In NGINX, the baseline number of hashes per server is hardcoded to 160, and Pingora uses the same value as the default. I’ll spare you the math for now, but if we go back to our 100-server example, if we use 160 points per server instead of just one, the coefficient of variation (which we can think of like an error margin) drops from about 99% to about 8%, a significant improvement.

What if we add more hashes?

We saw above that increasing the number of hashes per server by a constant amount allows us to improve how evenly workloads are distributed per server, but what if we don’t want to distribute the work evenly? In Cloudflare’s case, we have some servers that have more storage space than others, so it would be better to have the number of requests allotted to a server be proportional to its disk space. One way to accomplish this is with the ketama algorithm. The naming is a little funny because the algorithm is named after the library where it was first implemented, and the library was named … well you can google it 😶‍🌫️.

For us, since we want workload to be scaled based on storage, we can use the disk space as the weight, which is exactly what the Pingora team has been doing for years. Elsewhere in the company where workloads are more compute-intensive, weights might be based on CPU or GPU count.

What if we add even more hashes???

The last problem we need to address is that so far we are working under the assumption that any server can handle any request, but in practice that is not the case. Things like compliance requirements or enabled caching features mean only a subset of servers can handle any particular request. Unfortunately, unlike before, we can’t solve this problem by adding more hashes to the same ring. We have to add completely new rings, and not only that — every combination of features potentially needs its own specific ring!

Storage improvements

One big improvement came from Zaidoon, who had an insight about our struct for storing hashes in PBR. That struct looks like this:

Unfortunately, Rust doesn’t make it that easy. Changing the size of the index as we did above does nothing to reduce the memory footprint. This is because Rust has alignment rules that require the size of a structure in memory to be a multiple of its largest (or “most aligned”) field. In this case, the hash is the largest with four bytes, so when stored in memory, a Point is required to have size $mN \times 4m$, so the minimum size is eight bytes.

Luckily there are well-known ways around this. You (meaning me) might be tempted to use #[repr(packed)], but that is controversial for good reasons. A safer but less readable solution is to store the hash and index as raw byte array and access them with getters. Both methods compile to the same thing.

This simple (if wordy) change reduces the amount of memory used for consistent hashing by a whopping 25%! In order to do better than that, we’ll need to jump back into the math, so everybody hang on to something; this is the home stretch.

What if we tried fewer hashes?

You may have noticed that we gave the formula for the standard deviation for the case where there is only one hash per server. Deriving the formula for the case where there are $m k m$ hashes per server is not easy, and most sources only give you an approximation or an asymptotic limit, but not us. I might not be a statistician, but I grew up with a calculus teacher (Hi, Mom!), and I wanted to know the actual value. The full derivation is in a supplemental post, but here is the payoff.

To see how increasing the hash count improves the accuracy, we need to look again at the coefficient of variation.

The predictions from my beautiful math only work if we think about hashes in a continuous ring, but in practice we use 32-bit numbers for the hashes that have the potential for collisions, and the probability of collisions goes up surprisingly quickly as the number of hashes increases (see the birthday paradox). Collisions matter because in the ideal case, every hash contributes to the volume and distribution of requests handled by the associated server, but a collision means some contributions are randomly dropped, introducing unpredictable error. If we compare some simulated results with 32-bit hashes with the predicted error rate, we can see that for data centers with 2048 servers, the error rate increases: between 10,000 and 100,000 hashes per server.

Ultimately, even though this realization feels kind of bad, it’s great news for our plan to reclaim some RAM! Now that we have some math to back it up, we determined that we could decrease the number of hashes we were generating for each server by 90% without incurring any appreciable error, so that is what we set out to do.

Migrating without melting origins

There was one more problem: changing the hash ring changes where some cacheable requests go. Even if the new ring is better, switching the whole network at once would effectively invalidate almost all cached content. It would turn a memory optimization into an apocalyptic increase in origin traffic.

So we did not make this a single global flip. For a while, PBR carried both versions of the cacheable load balancer in memory: the old ketama ring and the new smaller one. Each request used our normal migration framework to decide which ring should select the backend. That meant the rollout decision was stable per request hash, and it also gave us a clean rollback path. If anything looked wrong, we could send new requests back through the old ring without redeploying PBR.

We then rolled the migration out in layers. We started with small validation locations, moved through progressively larger groups of data centers, and only then continued toward the rest of the world. 

The important part was that we controlled two dimensions independently: how much traffic used the new ring, and where that traffic was allowed to move. A plain global percentage rollout would have spread cache churn everywhere at once. Data-center-scoped rollout kept the blast radius small and made it much easier to tell whether a change was actually safe.

During the migration, we watched backend-selection traces, ring-version counters, PBR connection errors, process memory, startup time, cache behavior, and origin traffic. Once the migration reached 100%, we removed the temporary old-ring path, and voila!

The chart above shows the comparison of the memory used by PBR the week of the change compared with data from a few weeks before, as well as the result of subtracting one from the other. The sharp drop is the day where the version of PBR with the large (now unused) hash rings was decommissioned forever. Looking at the difference, we get the satisfying result that our changes dropped the used memory by 100TB!

Try it yourself

All the changes we talked about in this post are available now in the pingora-ketama crate in the form of a (for now) unadvertised cargo feature. The v2 ring has the compacted storage format, a faster sorting method, and the ability to scale the base number of hashes per node. Our focus in making these changes had to be on stability and control, so the v1 ring is identical to what pingora ketama has always used, and the library makes it possible to run both simultaneously and decide on a request-by-request basis which to use and when. 

Beyond trying our literal consistent hashing changes, I would like you to take away from this some inspiration to dig into your own systems to see what “simple” or “obvious” decisions are hiding potential wins, if you’re willing to get into the numbers. You might not be able to solve all your problems with Rust, but math is universal.

Дубайбад – бившото Аудиовидео Орфей в Изгрев

Post Syndicated from Боян Юруков original https://yurukov.net/blog/2026/dubaibad2/

Днес излезе заявление за издаване на ПУП на имот от 26 декара в кв. Изгрев на бившото Аудиовидео Орфей. Писах преди, че заради бездействие на министерството на културата и липса на отношение от районната администрация държавата загуби имота.

През май дадох пример в картата на застрояването как би изглеждала сградата, ако се използват максимално позволените параметри. Скицата към заявлението от днес показва много по-лоши планове. Както винаги казвам откакто се вгледах в застрояването – ако се питате дали ще се застрои, то отговорът е да и ще е по-зле отколкото си представяте. Не просто надвишават разрешените параметри и практически няма място за озеленяване, но и ще блокират улиците в района.

Около имота има само малки квартални улици направени за няколко десетки семейства живеещи в двуетажни къщи. В случая говорим за стотици отгоре на това. Първият изход е по ул. Самоков в посока кръговото на 4-ти км. Преди това се превръща в ул. Тинтява и това кръстовище вече е изцяло блокирано сутрин и вечер. Другият път е към метроспирката на Кюри. Кръстовището там е по-малко блокирано, но не е направено за такъв трафик, нито малките квартални улици, през които ще минават стотици коли в повече всеки ден.

Нямам съмнения, че ще си купят обаче транспортен анализ с удобните изводи. Видяхме го при няколко строежа в близост. Тези не са публични все още макар да целят защита на обществените интереси и безопасност. За абсурдните твърдения в тях научаваме единствено от дискусиите в комисия на НАГ.

Имотът е 26195, с кинт. 3.5 и максимална височина 75 метра. Дори предполагайки щедри като височина фоайета и големи студия, каквито се предвиждат в сделката с Аудиовидео Орфей, пак скицата ни показва разгърната площ от 30% повече от разрешеното по устройствен план. Единственият начин това да стане е да си купят гласове от ГЕРБ, БСП и ВМРО в СОС, които да им го позволят. Височината също се надвишава на едно място и предвижда почти 80 м.

Озеленяването трябва да е 40%, от които една четвърт да са високи дървета. Скицата показва 50% застроена площ. Като включим тротоари, алеи, входове на гаражи, трансформатори и друга инфраструктура, трудно може да се види как биха постигнали 40% озеленяване на така представената скица. Отчитайки отстоянията между сградите, практически е невъзможно да поместят и 210 дървета, какъвто е минимума, които да оцелеят и да имат задължителните по наредба отстояния от сгради и бордюри. От месеци има сигнали, че сегашните дървета се изсичат незаконно. Районната община мълчи въпреки сигналите. Няма разрешение за строеж и основание да им го разрешат, а те и не публикуват такива разрешения, макар да са длъжни по наредба. Бързат да секат обаче, защото биха били пречка за бъдещото разрешение за строеж.

А ще е пречка, защото според същата скица ще бетонират и запечатат изцяло 26-те декара за изграждането на 2 етажа подземен гараж. Това, както и масовата практика в (не)озеленяването в София, за която дадох примери. Единият беше за готови сгради, а другият – за строящи се съвсем наблизо. Отново, не очаквам да е проблем дори да не боднат едно дръвче и пак да вземат акт 16. Сега водя две дела с ДНСК, които отказват да предоставят административен документ показващ кой е подписал, че всичко е наред с озеленяването на две готови сгради с ясно видими нарушения. Не очаквам нещо различно тук.

Проектът е още на етап заявление за ПУП. Не е прието или одобрено. Въведох го в сегашния му вид на картата на застрояването, защото това е планът на инвеститора. С други думи, общината нищо не е разрешила. Научаваме за ПУП-а заради прозрачност, която Терзиев въведе в началото на миналата година, точно, за да знаем за подобни планове и да реагираме, защото именно този етап е критичен и до сега оставаше скрит.

Тепърва ще минава комисия, ще се иска становище от районната община и експерти. Районният кмет отговаря на такива проекти винаги без забележки и съгласие до сега като пример е проекта на Тинтява 80, сега започвания в съседство и вече с проблеми строеж на бившите сервизи бензиностанция Петрол, както и кулите на мястото на сградата на ПИБ. Та не очаквам много от там. Надеждата ми е, че комисията в НАГ ще изиска значително намаление на измеренията на проекта с оглед на инфраструктурата и цялостното планиране на квартала. Ще следим обсъжданията, но именно на нито ПУП е важно какво се реши, а не да се действа както в миналото на парче.

Leave the Class Path in the Rearview Mirror

Post Syndicated from Netflix Technology Blog original https://netflixtechblog.com/leave-the-class-path-in-the-rearview-mirror-67a85b15b6be

Introducing composable, module system native and agent friendly command line tools for modern Java development

By Danny Thomas, JVM Ecosystem Team

Recent work on the Java language to pave the on-ramp has made it easier than ever to start a Java program and evolve it using the full language and platform. At the end of that on-ramp lies Java’s mature build and dependency management ecosystem, capable of carrying software to enormous scale and complexity.

That ecosystem reached its maturity by developing strong models for projects, dependencies, and builds. When the Java Module System arrived, those models were already serving developers exceptionally well. The module descriptor consequently became just another description of the project to keep in agreement.

We’re excited to announce a preview of ja and its family of composable tools, that build on the capabilities of the Java Module System to provide a modern command line development experience for Java. We take the module descriptor and make it a complete description of a project, with dependency versions sitting naturally beside its requires directives and module metadata provided through documentation tags:

/**
* @mainClass com.example.application.Main
*/
module com.example.application {
requires com.example.framework; // @1.2.3
}

Combined with command line ergonomics you’re used to in other languages, creating and consuming Java modules has never been easier.

Composable Tools

Java developers have long been exceptionally well served by graphical tools. An IDE formats source, navigates between declarations and usages, presents API documentation, and maintains a compiled view of the project. That experience has been so complete that Java has had less need to expose the same capabilities through small, composable command line tools. Those gaps become quickly apparent when coding agents work with the Java language, with agents frequently struggling to locate dependencies, documentation and sources.

ja only provides command line ergonomics and tool orchestration, each feature is underpinned by a standalone tool. You don’t need to adopt ja to get the benefit of these tools, you can compose them in any way you choose:

  • jig performs module version resolution, compilation and assembly, outputting standard module system arguments for use with other tools. It is also the bridge to and from Maven repositories providing a standalone module proxy and publishing commands
  • jfmt formats source using the Code Conventions for the Java Programming Language, adapted for the modern Java language. Avoids the very common whitespace, indentation, import ordering and qualified class references introduced in agent written code
  • jist provides source aware symbol search, providing a grep style interface for understanding class files and their associated sources. Gives coding agents access to symbols and sources without indexing, LSPs or MCPs while interoperating with other build tools via an argument file contract
  • jdocserver serves locally browsable API documentation

These projects use the tool discovery and execution capabilities of the platform, and are intended to be installed in your JDK along with the standard tools. They all implement Tool or ToolProvider, allowing them to be run in process.

This is also the tool discovery and execution model for ja. There we use OptionChecker and optional custom metadata to discover which module system options are supported so it can resolve the arguments on behalf of the tool. This provides a seamless transition from your source path modules to the standard JDK tooling such as jdeps, jlink and jshell.

Maven as a foundation

In a recent survey of the 1,000 most popular artifacts on Maven Central, just 232 had explicit module definitions and another 248 declared automatic module names. The remaining 520 expressed no Java module name opinion. The module system also makes no distinction between namespace and module name, so module-first tooling requires a solution to module naming and location in existing repositories.

Fortunately, Maven Central already gives published artifacts a verified namespace. Publishers prove control of reverse domain group IDs, reflecting Sonatype’s long standing case for namespaces in public repositories.

We use these conventions to establish a canonical Maven module coordinate, paring a verifiable DNS namespace with the complete module name, for example pkg:maven/com.netflix/com.netflix.tools.ja. For existing modules, authors choose to publish a single Maven relocation pom at the canonical coordinate, to allow for discovery of the original coordinate.

When neither are available, candidates are walked from the root of the namespace using common Maven artifact conventions inferring coordinates from module names. We also bundle a short list of aliases for the most popular modules that don’t use a reverse DNS module name, but we suggest authors should always namespace their modules. The module proxy in jig presents resolved modules using the filename based conventions for module naming, making even automatic modules without stable names safe when used with these tools.

These conventions and location strategies allow the majority of existing artifacts to be discovered using only the module name and version.

Integrity by default

ALL-UNNAMED has become unfortunately common in Java access options, because of the heavy use of the class path. It hides the source of the technical debt that applications are incurring by allowing such access and becomes increasingly consequential as Java moves toward Integrity by Default. For example, Preparing to Make Final Mean Final asks applications to explicitly authorize the modules allowed to mutate final fields.

We allow runtime access requirements to bedeclared as module metadata and carried with the module descriptor throughout the module’s lifecycle. For example a library may record the access it requires:

/**
* @enableFinalFieldMutation com.example.framework
*/
module com.example.framework {
}

However, the consuming application remains in control and must explicitly authorize the framework, for it to be available at runtime:

/**
* @mainClass com.example.application.Main
* @enableFinalFieldMutation com.example.framework
*/
module com.example.application {
requires com.example.framework; // @1.2.3
}

The command line interface for ja allows the dependency and authorization to be added together:

ja require [email protected] \
--enable-final-field-mutation com.example.framework

Without that authorization, dependency resolution fails with an unsatisfied access requirement. Native access follows the same model through @enableNativeAccess and qualified exports and opens are also supported.

Module integrity is ensured by persistent hashes of resolved binary dependencies in a module-info.hash file, sequent resolution verifies those hashes and rejects an artifact that has changed.

We also take a step further than the recent improvements to annotation processor security by treating annotation processing as an explicit code generation step. The resulting sources are alongside regular module source, making them visible in code review and allowing a module to be assembled without executing generator code.

Make modules your default

We think every Java project should be modular, regardless of the build tool you’re using. If you’re a library author producing automatic modules, we’d encourage you to avoid split packages and produce explicit modules.

You can get started with our tools today with our installation guide.


Leave the Class Path in the Rearview Mirror was originally published in Netflix TechBlog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Running self-hosted AI agent sandboxes with AWS Lambda MicroVMs

Post Syndicated from Brian Krygsman original https://aws.amazon.com/blogs/compute/running-self-hosted-ai-agent-sandboxes-with-aws-lambda-microvms/

Organizations are building AI agents that autonomously write code, query databases, and interact with internal systems on behalf of their teams. These agents handle use cases such as automated code review, data pipeline optimization, and infrastructure troubleshooting. When your AI agent generates a shell command, queries a database, or writes to a file system, that code needs a secure environment to run in. Without isolation, one session’s tool calls can contaminate another session’s state, inadvertently expose sensitive data across tenants, or unintentionally allow untrusted code to reach production resources. Self-hosted sandboxes solve this by keeping agent execution within your own AWS account, giving you full control over networking, secrets, and governance.

Say you’re building an internal AI agent that optimizes database queries for your engineering team. A developer asks it to find the ten slowest queries in your analytics database, rewrite them with better indexing, and test the results. That’s three tool calls in a single session. One hits a live database with real credentials. One generates code. One executes it. Now multiply that by fifty developers using the assistant at the same time. Each session needs its own credentials, its own filesystem, its own network boundary. If credentials or state cross session boundaries, you have inadvertent data exposure.

AWS Lambda MicroVMs is a serverless compute environment that provides general-purpose runtimes with the strong isolation of virtual machines and the rapid scaling of AWS Lambda. Powered by Firecracker virtualization, each MicroVM runs Amazon Linux with full OS access for up to 8 hours. You launch, suspend, resume, and terminate MicroVMs programmatically. You get the serverless benefits of managed infrastructure, responsive scaling, and pay-per-use pricing. Three capabilities make Lambda MicroVMs a strong fit for agent sandboxes:

  • VM-level isolation per environment: Each MicroVM runs in its own Firecracker virtual machine, providing hardware-virtualization-based isolation between sessions without the resource overhead and startup time required of full VMs. One developer cannot see a teammate’s session, even when both run at the same time.
  • Launch from snapshot: Like Lambda SnapStart, MicroVMs boot from a pre-captured memory and disk snapshot, skipping application initialization entirely. Your agent gets a near-instant ready-to-use environment.
  • 4x vertical scaling without re-provisioning: A running MicroVM can scale CPU and memory up to 4x its initial allocation, which can range from 0.25 vCPU/0.5 GB to 4 vCPU/8 GB, without terminating or re-creating the environment. If the agent needs to run a heavy data transformation mid-session, it can get more resources without starting over.

In this post, we show you how to architect and build a self-hosted AI agent that uses Lambda MicroVMs as secure, isolated sandboxes for tool-call execution. Lambda MicroVMs can handle the compute isolation for running tool calls, while the host for production AI agents, such as Amazon Bedrock AgentCore, manages the agent logic, model routing, and session state. A complete reference solution is available in aws-samples.

How self-hosted sandboxes work

A developer asks the agent to “find the ten slowest queries in our analytics database and suggest index improvements.” The agent orchestration system starts a session then breaks the objective into tool calls and distributes them. A worker needs to pick up that session, run the queries, and return results.

Most AI agent orchestration services and frameworks use a work queue model to distribute tool-call execution. The orchestration service enqueues sessions representing tool-call work. A worker, the process that claims a session and executes its tool calls, runs inside a compute environment, posts results, and exits. In this architecture, each Lambda MicroVM is the compute environment, and the worker is the process running inside it. Claude Managed Agents self-hosted sandboxes run those workers inside your own infrastructure rather than on a shared, multi-tenant compute pool. Your database credentials stay in your virtual private cloud (VPC). Your network, introspection, and governance rules apply.

You can trigger workers in two ways:

  • Webhook-triggered: The orchestration application sends a notification when a session is ready. Your control plane launches a worker on demand.
  • Always-on: A long-running process continuously polls the work queue for new sessions.

The Lambda MicroVMs lifecycle aligns with the webhook-triggered pattern, where each session produces one inbound event that launches a fresh MicroVM. Lambda MicroVMs support configurable idle policies. After a configurable idle period, a MicroVM suspends automatically, preserving disk and memory state. It resumes when inbound traffic arrives or when you call the resume API. The MicroVM runs for the duration of the session, the worker exits, and the idle policy suspends then finally terminates the VM. Lifecycle hooks allow you to run custom logic at key steps in the MicroVM lifecycle.

In contrast, the always-on pattern risks breaking the polling loop by suspending the MicroVM when idle, since there’s no inbound traffic between sessions. You could disable the configurable idle period, but then you pay for empty polling. Use the webhook-triggered approach for self-hosted sandboxes on Lambda MicroVMs.

Architecture

The following figure shows the reference solution’s architecture, with the Anthropic agent orchestration service control plane on the left interacting with a self-hosted sandbox environment in AWS on the right.

Reference architecture showing the Anthropic orchestration control plane sending a webhook through API Gateway to a launcher Lambda function that starts a MicroVM worker in your AWS account

Figure 1: Reference architecture for self-hosted AI agent sandboxes on Lambda MicroVMs

The sample architecture is event-driven. The only inbound traffic is the webhook call. When the event arrives, the handler launches a MicroVM. Once launched, the MicroVM pulls its assigned session from the orchestration system’s work queue and runs the task. In our example, the developer’s “find slow queries” request has been queued as a session. The agent now needs to reach your infrastructure, spin up an isolated environment, and hand off the work. The following sequence shows how each component interacts to fulfill a single session.

The orchestration service queues work as sessions. A MicroVM launches to service each session, and the worker is the process running inside that MicroVM that claims the session, executes tool calls, and returns results.

  1. Once the orchestration service marks a session as ready to run, it sends a session.status_run_started webhook to an Amazon API Gateway endpoint, triggering a MicroVM launch.
  2. The launcher verifies the webhook signature using a signing secret from AWS Systems Manager Parameter Store, rejecting invalid or stale deliveries before spending compute.
  3. The launcher calls RunMicrovm, passing the session ID and a secret reference through runHookPayload. It deduplicates on the webhook event ID (backed by Amazon DynamoDB) so retries do not launch duplicate VMs.
  4. The MicroVM boots from a pre-captured Firecracker snapshot and receives the dispatch on its /run lifecycle hook. The worker fetches the environment key from Parameter Store using its execution role. It pulls the matching session from the work queue, claims it, and executes tool calls in an isolated /workspace directory. When finished, it posts results and exits. The idle policy suspends then terminates the VM.

Deduplication. The webhook event ID serves as the idempotency key. The launcher uses Powertools for AWS Lambda (Python) with a DynamoDB persistence layer to verify exactly-once processing. If the orchestration application retries a delivery with the same event ID, Powertools protects the system from launching extra MicroVMs and doing extra work.

Credential boundaries. Each component accesses only the single secret it needs. The launcher reads only the webhook signing secret to verify inbound events. It passes only an ARN reference to the environment key into the MicroVM payload. The MicroVM’s execution role retrieves only that environment key at runtime. No single component holds both secrets.

Component Has access to
Launcher Lambda Webhook signing secret (verify inbound events)
MicroVM worker Environment key (through the execution role, to poll and claim sessions)

Cost model. You pay for MicroVM run time per session, plus standard charges for API Gateway requests, Parameter Store API calls, and Lambda invocations for the launcher. When no sessions are active, no MicroVMs run. Cost scales with concurrent sessions and their duration, avoiding idle compute charges.

Implementation

The following sections explore the reference architecture in more depth.

Project structure

The reference solution uses AWS Serverless Application Model (AWS SAM) for infrastructure-as-code. Alternatively, if you use an AI coding agent such as Claude Code, Kiro, or Cursor, the Agent Toolkit for AWS includes a Lambda MicroVMs skill that gives your agent the procedures to provision, configure, and deploy MicroVM-based sandbox environments on your behalf.

├── template.yaml                    # SAM: launcher, API, WAF, secrets, roles
├── src/
│   ├── functions/launcher.py        # Verify signature, RunMicrovm
│   ├── microvm-image/
│   │   ├── Dockerfile               # AL2023 + Node.js worker
│   │   └── worker/worker.mjs        # Lifecycle hook server
│   └── scripts/build-image.sh       # Package + create MicroVM image

Launcher: verify the webhook before spinning up compute

When the webhook arrives saying a developer’s session is ready, the launcher’s first action is signature verification. If it fails, the function returns 401 immediately. No MicroVM launches. No DynamoDB writes. You don’t pay for fraudulent or replayed requests.

signing_secret = [REDACTED_PASSWORD]  # Verify webhook before spending compute
if not verify_signature(raw_body, headers, signing_secret):
    return {"statusCode": 401, "body": "invalid signature"}

After verification, the launcher builds a dispatch payload containing the session ID, environment ID, region, and an ARN reference to the environment key secret. It passes this to RunMicrovm through runHookPayload:

launched = microvm_client.run_microvm(
    image_identifier="arn:aws:lambda:us-east-1:123456789012:microvm-image:worker",
    run_hook_payload=json.dumps({"session": dispatch}),
    execution_role_arn=config.execution_role_arn,
    maximum_duration_in_seconds=28800,
    ingress_network_connectors=["arn:aws:lambda:::network-connector:aws-network-connector:ALL_INGRESS"],
    egress_network_connectors=["arn:aws:lambda:::network-connector:aws-network-connector:INTERNET_EGRESS"],
)

MicroVM worker: claim one session, execute, exit

The MicroVM image is built from a Firecracker snapshot. The worker process starts during image creation and is captured in the snapshot, so there is no application startup at run time. The /run lifecycle hook delivers the dispatch payload:

// POST /aws/lambda-microvms/runtime/v1/run
case "run": {
    const envelope = JSON.parse(rawBody);
    const dispatch = JSON.parse(envelope.runHookPayload);
    res.writeHead(200); // Acknowledge hook immediately
    res.end();
    const key = await fetchParameter(dispatch.session.ENVIRONMENT_KEY_PARAM_NAME);
    await pollAndHandleSession(dispatch.session.ANTHROPIC_SESSION_ID, key);
    // Session complete; terminate this MicroVM to release all resources
    await terminateMicroVm(envelope.microvmId);
}

The worker acknowledges the hook within its timeout, fetches the environment key, and claims the session. This is where the requested work begins. The worker connects to the analytics database, runs EXPLAIN ANALYZE on the flagged queries, writes optimized alternatives to /workspace/suggestions.sql, and posts the results back to the developer. All of that happens inside this single VM. When the session completes, the worker calls terminate-microvm to release all compute resources.

Deployment

For full deployment instructions, see the reference solution README. Before deploying, make sure you have these prerequisites.

Prerequisites

Four steps

  1. Deploy the control plane. Build and deploy the SAM stack, which creates the launcher Lambda, API Gateway endpoint, WAF WebACL, DynamoDB idempotency table, Parameter Store entries, and MicroVM execution role.
    sam build
    sam deploy --guided --capabilities CAPABILITY_NAMED_IAM

  2. Register the webhook and populate secrets. In the Claude Console, register the stack’s WebhookUrl output as a webhook endpoint subscribed to session.status_run_started. Store the signing secret and environment key in the Parameter Store resources created by the stack.
  3. Build the MicroVM image. Package the Dockerfile and worker code, upload to Amazon S3, and create the image. The service runs your Dockerfile, launches the worker, and captures a Firecracker snapshot. Monitor build progress in Amazon CloudWatch under /aws/lambda/microvms/<image-name>.
    ./src/scripts/build-image.sh

  4. Verify. Create a test session and confirm a MicroVM launches and completes end-to-end. The reference solution includes a verification script that creates a session, triggers the webhook, and validates the full flow.

Using Claude Platform on AWS (CPOA)

The preceding architecture works similarly when you access Claude through Claude Platform on AWS rather than the first-party API. Three things change in the worker:

  1. Client initialization. Replace the first-party client with the AWS client and supply your workspace ID:
    from anthropic import AnthropicAWS
    
    client = AnthropicAWS(aws_region="us-east-1")
    
    # Workspace ID is required on every request
    # Set via ANTHROPIC_AWS_WORKSPACE_ID env var or pass per-call

  2. Authentication options. CPOA supports two modes:
    1. CPOA API key (aws-external-anthropic-api-key-...): Store it in Parameter Store the same way as the first-party environment key. These keys are short-lived (12-hour STS tokens) and must be regenerated when they expire.
    2. SigV4 (IAM): The MicroVM execution role can sign requests directly, so there is no secret to store or rotate. Set the environment key secret to a placeholder value (for example, use-sigv4) and the SDK falls through to IAM credentials automatically. This is the recommended path for production.

In both authentication modes, attach the AWS managed policy AnthropicSelfHostedEnvironmentAccess to the MicroVM execution role. This policy grants the aws-external-anthropic actions needed to poll the work queue, claim sessions, and post results. See IAM actions for Claude Platform on AWS for the full reference.

Prerequisite: Enable outbound web identity federation once per AWS account:

aws iam enable-outbound-web-identity-federation

Everything else, including webhook verification, deduplication, credential separation, and idle policy remains the same.

Security

Earlier we talked about what goes wrong without isolation. Credentials exposed between sessions. Scripts unintentionally reaching production. Agents escaping their sandbox. This architecture implements defense in depth to help prevent these.

Each component accesses a single, scoped secret. The launcher passes only an ARN reference to the worker credential into the MicroVM. The MicroVM’s execution role retrieves only that credential at runtime. The analytics database connection string does not touch the launcher and does not leave your environment.

AWS WAF applies managed rule sets (OWASP, known bad inputs, IP reputation) and per-IP rate limiting. Amazon API Gateway request validation rejects malformed bodies. The launcher performs HMAC signature verification as the true authentication boundary.

Each session runs in its own MicroVM. Sessions do not share memory, disk, or network namespaces. Firecracker provides hardware-virtualization-based isolation. The launcher IAM role reads only the signing secret. The MicroVM execution role reads only the worker credential. Both are scoped to specific Parameter Store ARNs. The Amazon S3 artifact bucket blocks public access, enables versioning, and uses server-side encryption.

Conclusion

This post walked through how to give your internal AI agent a safe place to run database queries, generate code, and execute scripts on behalf of fifty developers without leaking data between sessions or reaching resources it shouldn’t.

AWS Lambda MicroVMs provide ephemeral, VM-isolated compute environments that align with the per-session execution model of AI agent sandboxes. Snapshot-based launch avoids application startup latency. Idle policies terminate VMs once sessions complete. Firecracker isolation verifies that sessions do not share state. You pay only for active execution time and maintain full control over credentials, networking, and governance within your AWS boundary.

You build and operate a serverless control plane. You get per-session VM isolation with no idle compute cost and no shared tenancy.

To get started, explore these resources:

The collective thoughts of the interwebz