DEV Community

Cover image for AWS Lambda Managed Instances: What Min=0/Max=0 Actually Does in Production
Ankur Jindal
Ankur Jindal

Posted on

AWS Lambda Managed Instances: What Min=0/Max=0 Actually Does in Production

A hands-on walkthrough, including the exact failures and dead ends you'll hit setting this up.


Introduction: The Production Bottleneck

AWS Lambda Managed Instances (LMI) puts Lambda functions on EC2 capacity you control, while keeping Lambda's programming model. Most of the coverage since re:Invent 2025 has focused on two things: the removal of the memory ceiling, and the EC2-backed pricing model that lets you apply Savings Plans and Reserved Instances.

Almost none of it covers the scaling configuration — specifically, what happens when MinExecutionEnvironments and MaxExecutionEnvironments are both set to 0.

The assumption most engineers carry over from standard Lambda is that a minimum of zero means "no standing capacity, scale up from zero on the next request." That assumption is wrong for LMI, and it's wrong in a way that produces a hard invoke failure instead of a slow cold start. This post documents the actual mechanics, verified against a live AWS account, along with the setup friction that isn't in the AWS documentation.

The Mechanics of the Failure Chain

On Lambda Managed Instances, MinExecutionEnvironments and MaxExecutionEnvironments are not independent settings. AWS enforces a pairing rule: a minimum of 0 is only accepted when the maximum is also 0.

When both are set to 0, Lambda does not idle the function and wait for the next invocation. It deactivates the function version:

  1. Every EC2 Managed Instance backing that version is terminated.
  2. Instance charges continue until termination actually completes — the meter doesn't stop the moment you save the config.
  3. The version's State flips to Deactivated.
  4. Any invocation against a deactivated version returns an explicit error. There's no cold start, no queueing, no retry — it fails outright.
  5. Reactivation is never automatic. You have to push a new scaling configuration with non-zero values, through the console, the PutFunctionScalingConfig API, or a scheduled action.

This is the opposite of how ReservedConcurrentExecutions=0 or standard Lambda concurrency throttling behaves, and it's the single most important operational difference between LMI and normal Lambda for anyone running non-continuous workloads.

Production Architecture Pattern & Walkthrough

System Architecture Overview

LMI has one prerequisite that standard Lambda doesn't: a Capacity Provider. This is a separate resource that defines:

  • The VPC, subnets, and security groups the EC2 instances launch into
  • Which instance architecture (x86_64 or arm64) and types are eligible
  • The scaling mode (Auto or Manual)
  • The IAM role Lambda uses to manage EC2 on your behalf (the "Operator Role")

A function is then attached to a Capacity Provider at creation time by setting its compute type. Only after that attachment exists — and only on a published version, not $LATEST — do MinExecutionEnvironments and MaxExecutionEnvironments become meaningful.

Setup Walkthrough: Step by Step

1. Create the Capacity Provider and attach it to the function.

In the Lambda console, this doesn't appear as "Compute type" the way older documentation describes it — in the current console it's the EC2 capacity provider toggle under Custom settings. Toggling it on opens a side panel to select the capacity provider ARN, memory size, and the execution-environment-memory-per-vCPU ratio:

Capacity provider configuration panel

The panel already resolves the capacity provider by ARN once created. Memory and the vCPU ratio (2:1, 4:1, or 8:1) are set per-function here, not per-capacity-provider.

2. Architecture must match between the function and the capacity provider.

The first real failure: creating the function returned this error on save —

Architecture mismatch error

"You cannot use a Lambda Managed Instances function with a capacity provider that does not support the architecture of the function."

The capacity provider's InstanceRequirements.Architectures is fixed at creation time (in this case, arm64). If the function's own architecture toggle doesn't match, function creation is rejected outright — there's no coercion or fallback. Confirmed via CLI:

{
    "CapacityProvider": {
        "CapacityProviderArn": "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:capacity-provider:lambaprovisionedinstance",
        "State": "Active",
        "VpcConfig": {
            "SubnetIds": [
                "subnet-<redacted>",
                "subnet-<redacted>"
            ],
            "SecurityGroupIds": [
                "sg-<redacted>"
            ]
        },
        "PermissionsConfig": {
            "CapacityProviderOperatorRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/service-role/lambaprovisionedinstance-role-<redacted>"
        },
        "InstanceRequirements": {
            "Architectures": ["arm64"]
        },
        "CapacityProviderScalingConfig": {
            "ScalingMode": "Auto"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Fix: match the function's architecture toggle to whatever the capacity provider was created with. There's no cross-architecture capacity provider.

3. Publishing does not prompt for scaling configuration.

Creating and publishing the function does not ask for MinExecutionEnvironments or MaxExecutionEnvironments at any point. That configuration lives separately, under Configuration → Function scaling configuration, edited independently after the function exists:

Default scaling configuration — no minimum, no maximum set

With nothing configured, AWS's default behavior applies: 3 Managed Instances provisioned across availability zones before the version is marked Active, invisible in this panel until you explicitly edit it.

4. $LATEST is not invocable on LMI — only a published version is.

This is a second point of confusion that isn't obvious from the console. Querying $LATEST directly shows a state most engineers won't recognize:

{
    "Configuration": {
        "FunctionName": "lmi-function",
        "FunctionArn": "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:lmi-function:$LATEST",
        "State": "ActiveNonInvocable",
        "Architectures": ["arm64"]
    }
}
Enter fullscreen mode Exit fullscreen mode

ActiveNonInvocable is a real, documented state (FunctionConfiguration valid values: Pending | Active | Inactive | Failed | Deactivating | Deactivated | ActiveNonInvocable | Deleting). It means exactly what it says — $LATEST cannot run instances on LMI. Only a published version gets EC2 capacity.

LMI introduces $LATEST.PUBLISHED specifically for this: a version you can republish repeatedly without managing numbered versions. When created through the console, this is generated automatically. Querying it directly confirms the actual invocable state:

{
    "Configuration": {
        "FunctionName": "lmi-function",
        "FunctionArn": "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:lmi-function:$LATEST.PUBLISHED",
        "Version": "$LATEST.PUBLISHED",
        "State": "Active",
        "Architectures": ["arm64"]
    }
}
Enter fullscreen mode Exit fullscreen mode

Invoking the function via its unqualified ARN implicitly targets $LATEST.PUBLISHED, not $LATEST — the reverse of how unqualified invokes behave on standard Lambda.

5. Managed Instances are hidden from the EC2 console by default.

Once $LATEST.PUBLISHED showed Active, the expected 3 instances weren't visible anywhere in the EC2 console, and describe-instances with a capacity-provider tag filter returned nothing:

{
    "Reservations": []
}
Enter fullscreen mode Exit fullscreen mode

The cause: Managed resource visibility, an account-wide EC2 setting (introduced April 2026) that hides EC2 resources provisioned by services like LMI, EKS Auto Mode, and ECS Managed Instances from the console and Describe* API calls by default for accounts that didn't have managed resources before the setting existed. The instances are running and billing regardless — visibility is purely a display filter.

Fix, via CLI:

aws ec2 modify-managed-resource-visibility --default-visibility visible
Enter fullscreen mode Exit fullscreen mode
{
    "Visibility": {
        "DefaultVisibility": "visible"
    }
}
Enter fullscreen mode Exit fullscreen mode

This is account-wide and Region-specific, and applies to all managed-resource offerings, not just LMI. After setting it, the instances appeared after a while in the EC2 console:

EC2 console showing the 3 provisioned Managed Instances

Two running, one terminated — consistent with AWS's documented default of 3 instances provisioned for availability across AZs, with normal instance replacement happening in the background.

6. Verifying the 0/0 deactivation.

Editing the Function scaling configuration to MinExecutionEnvironments=0 and MaxExecutionEnvironments=0 and saving triggers an immediate, visible state transition in the console:

Deactivating version banner

"Deactivating version $LATEST.PUBLISHED."

Within a few minutes, all three EC2 instances move to Terminated, and invoking the function returns an explicit error referencing the deactivated state — not a timeout, not a cold start, not a retry. This is the core behavior the rest of this post is about: 0/0 is a deactivation switch, not a scale-to-zero setting.

Implementation Strategies & Deep-Dive Considerations

Pricing changes the idle-cost math. LMI drops Lambda's per-invocation duration billing entirely. Instead, pricing has three components: the standard $0.20 per million requests, standard EC2 instance charges (Savings Plans and Reserved Instances apply), and a flat 15% management fee on top of the on-demand EC2 price — applied even when Savings Plans or RIs are discounting the underlying instance cost. That fee doesn't pause during idle traffic. It only stops when instances are actually terminated, which only happens through deactivation or explicit scale-down.

For a workload with, say, 5 hours of daily invocation activity, this means: unless MinExecutionEnvironments is actively brought to 0/0 during the other 19 hours, you are paying full EC2 rate plus the management fee for the entire idle window — not "only for the 5 hours," the way standard Lambda duration billing would imply.

There's no scale-out SLA. AWS does not publish a guaranteed scale-out latency for LMI. The explicit documented warning is that traffic more than doubling within a 5-minute window can produce throttling while capacity catches up. Combined with the 0/0 deactivation behavior, this means bursty or unpredictable workloads should generally avoid 0/0 entirely — the risk isn't a slow response, it's outright invoke failures during both the reactivation window and any sufficiently sharp traffic spike even at steady-state minimums.

EventBridge Scheduler is the correct pattern for predictable idle windows, not manual toggling. A nightly batch job or a fixed 9-to-5 traffic pattern is exactly the case 0/0 is designed for — scheduled deactivation before the idle window, scheduled reactivation before traffic resumes, via PutFunctionScalingConfig calls triggered on a schedule rather than through the console.

Managed resource visibility is a one-time account setting, not a per-function toggle. Worth setting to visible early in any LMI adoption, since the default-hidden behavior for new accounts otherwise makes basic troubleshooting (like confirming instance count) unexpectedly difficult.

Architectural Tradeoffs & Operational Takeaways

Workload shape Recommended scaling config Why
Predictable idle windows (nightly batch, fixed business hours) 0/0 + EventBridge Scheduler Stops EC2 + management-fee billing during confirmed idle time; reactivation is scheduled, not reactive
Bursty or unpredictable traffic MinExecutionEnvironments > 0 Avoids hard invoke failures during the deactivated window and reduces exposure to the "traffic doubles in 5 minutes" throttling case
Continuous, steady-state traffic Standing non-zero minimum, sized to baseline You're paying for compute regardless; this is the workload LMI's EC2 pricing model is built for

The mechanical rule worth internalizing: MinExecutionEnvironments=0 is not a cost-saving default the way it is on standard Lambda concurrency settings. It only saves cost when paired with Max=0, and pairing it with Max=0 converts idle time into a hard failure window unless you're actively scheduling reactivation around it.

Top comments (0)