AWS Builder Center
Modern Container Builds and WebSocket APIs Come to AWS SAM

Modern Container Builds and WebSocket APIs Come to AWS SAM

Learn how SAM CLI's new BuildKit support and WebSocket API resource type simplify container builds and real-time APIs with fewer lines of YAML.

Principal Developer Advocate - Serverless @ AWS
SAM CLI has always been good at taking the grunt work out of serverless deployments. You define your functions and APIs in a template, and SAM handles the CloudFormation, the packaging, the deployment. It works.
Until recently, two things were missing.  BuildKit  support for container image builds. And a SAM resource type for WebSocket APIs.
Both are now shipped. Neither breaks existing templates.
Let's walk through it.

BuildKit Support for Image-Based Lambda Functions

The problem

When you run  sam build  for an image-based Lambda function, SAM uses the  docker-py  Python SDK under the hood. That SDK talks to the Docker daemon directly, but it doesn't support BuildKit. At all.
That means every  sam build  invocation uses the legacy Docker builder. You lose parallelized build stages. You lose efficient layer caching. You lose multi-stage build optimizations. You lose cross-architecture improvements that BuildKit has shipped over the past several years.
For simple single-stage Dockerfiles, this probably doesn't matter. For anything with multiple stages, private dependencies, or cross-compilation targets, it's a real bottleneck.

What shipped

 SAM CLI v1.156.0  introduced the  --use-buildkit  flag on  sam build . When you pass this flag, SAM bypasses docker-py entirely and shells out to the Docker CLI (or Finch CLI) directly. That gives you access to everything BuildKit offers.
sam build --use-buildkit

That's it. One flag.
SAM auto-detects which container runtime you have. It defaults to Docker, falls back to  Finch  if Docker isn't running, and respects any admin-configured preference. Finch supports BuildKit too, so either runtime works.

What you get

BuildKit brings real improvements over the legacy builder:
  • Parallel stage execution. Multi-stage Dockerfiles build independent stages concurrently instead of sequentially.
  • Better layer caching. BuildKit tracks dependencies at the file level, not just the layer level. Change one file, rebuild one layer.
  • Multi-stage build optimizations. BuildKit skips stages that don't contribute to the final output. The legacy builder runs every stage regardless.
  • Improved cross-architecture support. Building arm64 (Graviton2) images on an x86 machine is more reliable with BuildKit's QEMU integration. Lambda currently supports Graviton2 for arm64 workloads.

Build secrets

SAM CLI v1.159.0 added support for passing BuildKit parameters, including build-time secrets. This lets you pass credentials into a build stage without baking them into a layer. Useful when your Lambda function pulls packages from a private registry during the build.
# syntax=docker/dockerfile:1
FROM public.ecr.aws/lambda/python:3.12

RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
pip install -r requirements.txt

COPY app.py ${LAMBDA_TASK_ROOT}
CMD ["app.handler"]

You configure secrets through the  Metadata  section of your function resource, using  DockerBuildExtraParams . SAM passes these parameters straight to  docker buildx  under the hood.
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
PackageType: Image
Architectures: [x86_64]
Timeout: 10
Metadata:
Dockerfile: Dockerfile
DockerContext: ./src
DockerTag: latest
DockerBuildExtraParams:
- "--secret"
- "id=pip_conf,src=$HOME/.pip/pip.conf"

The secret mounts into the build stage at the path you specify in the Dockerfile, gets used during  pip install , and never appears in the final image layers. Different functions can have different build secrets, since the configuration lives in each function's  Metadata  block.

Tradeoffs and limitations

A few things to keep in mind.
Opt-in only. This doesn't change existing build behavior. If you don't pass  --use-buildkit , nothing changes. That's intentional. SAM doesn't break working builds.
Requires Docker or Finch CLI. BuildKit support works by calling the Docker or Finch CLI directly. If your CI environment only has the Docker daemon (no CLI), this won't work. Most environments have both, but check yours.
Dockerfile syntax matters. Some BuildKit features, like secret mounts, require the  # syntax=docker/dockerfile:1  parser directive at the top of your Dockerfile. Without it, the build falls back to legacy parsing and you get confusing errors.
Not for ZIP-based functions. This flag only applies to image-based Lambda functions (  PackageType: Image  ). ZIP-based functions don't use Docker at all.
The rule of thumb I like is this: if your Dockerfile is more than a few lines, or you're building for a different architecture, turn on BuildKit. The caching alone will save you time.

WebSocket API Support

The problem

Before this release, SAM had no native resource type for WebSocket APIs. You had two options: write raw CloudFormation, or don't use SAM for that part of your stack.
Here's what a minimal WebSocket API looks like in plain CloudFormation. Three routes ($connect, $disconnect, sendMessage), each backed by a Lambda function:
Resources:
WebSocketApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: MyWebSocketApi
ProtocolType: WEBSOCKET
RouteSelectionExpression: $request.body.action

ConnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: $connect
Target: !Sub integrations/${ConnectIntegration}

ConnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ConnectFunction.Arn}/invocations

ConnectPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref ConnectFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/$connect

DisconnectRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: $disconnect
Target: !Sub integrations/${DisconnectIntegration}

DisconnectIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DisconnectFunction.Arn}/invocations

DisconnectPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref DisconnectFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/$disconnect

SendMessageRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref WebSocketApi
RouteKey: sendMessage
Target: !Sub integrations/${SendMessageIntegration}

SendMessageIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref WebSocketApi
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SendMessageFunction.Arn}/invocations

SendMessagePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref SendMessageFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${WebSocketApi}/*/sendMessage

Deployment:
Type: AWS::ApiGatewayV2::Deployment
DependsOn:
- ConnectRoute
- DisconnectRoute
- SendMessageRoute
Properties:
ApiId: !Ref WebSocketApi

Stage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref WebSocketApi
StageName: prod
DeploymentId: !Ref Deployment

That's twelve resources for three routes. Around 90 lines of YAML. And I left out the Lambda function definitions.
Notice the  DependsOn  on the Deployment resource. If you forget that, CloudFormation tries to create the deployment before the routes exist, and the stack fails. You have to manage that dependency graph yourself.
That's a lot of ceremony for "connect a WebSocket to some Lambda functions."

What shipped

The new   AWS::Serverless::WebSocketApi   resource type collapses all of that into this:
Resources:
MyWebSocketApi:
Type: AWS::Serverless::WebSocketApi
Properties:
RouteSelectionExpression: $request.body.action
StageName: prod
Routes:
$connect:
FunctionArn: !GetAtt ConnectFunction.Arn
$disconnect:
FunctionArn: !GetAtt DisconnectFunction.Arn
sendMessage:
FunctionArn: !GetAtt SendMessageFunction.Arn

Compare that to the CloudFormation version. Twelve resources become one. Ninety lines become twelve. The routes, integrations, Lambda permissions, deployment, stage, and resource ordering are all handled by SAM's transform.
You define the routes. SAM generates the rest.

What SAM handles automatically

For each route you declare, SAM creates:
  • The  AWS::ApiGatewayV2::Route  resource
  • The  AWS::ApiGatewayV2::Integration  wiring the route to your Lambda function
  • The  AWS::Lambda::Permission  granting API Gateway invoke access
  • If you add a Lambda authorizer, the authorizer permission too
SAM also creates the deployment and stage resources, with the correct dependency ordering. No  DependsOn  blocks to manage.

Full feature parity

This isn't a simplified subset. The new resource type supports everything  API Gateway V2 WebSocket  offers:
  • Auth: IAM authorization and Lambda authorizers, per-route or API-wide
  • Custom domains: Map your WebSocket API to your own domain
  • Route settings: Configure throttling and logging per route via  RouteSettings 
  • Models: Attach request/response models for validation
  • Stage variables: Pass configuration to your integration through stage variables
  • Globals: Share configuration across multiple WebSocket APIs using the SAM  Globals  section

Use cases

WebSocket APIs are the right tool when you need a persistent, bidirectional connection. Common patterns:
  • Chat applications. Users send and receive messages in real time.
  • Live dashboards. Push metric updates to connected browsers without polling.
  • AI/LLM streaming. Stream token-by-token responses from a model back to the client. This one is increasingly common.
  • IoT command channels. Send commands to devices and receive status updates on the same connection.
If your current approach is a REST API that the client polls every few seconds, a WebSocket API will give you lower latency and lower cost. Fewer requests, fewer Lambda invocations, faster updates.

Tradeoffs and limitations

No local emulation. SAM CLI doesn't support  sam local start-api  for WebSocket APIs. You can test individual Lambda handlers with  sam local invoke , but end-to-end local WebSocket testing isn't available yet. Deploy to a dev stage for integration testing.
No  sam local start-websocket . Related to the above, there's no dedicated local command for WebSocket APIs like there is for HTTP APIs with  sam local start-api .

For the Agents

If you're using an AI coding agent to build SAM applications, both of these features work out of the box with agent-driven workflows. Your agent can scaffold a WebSocket API or add BuildKit to an existing image-based function without any special setup.
For  Kiro , there's an official AWS SAM Power that gives your agent SAM-aware tooling. Install it and your agent gets access to  sam_init ,  sam_build ,  sam_deploy ,  sam_logs , and  sam_local_invoke  as callable tools, plus opinionated project structure guidance.
Here's what a Kiro-assisted WebSocket API scaffold looks like in practice:
You: Create a WebSocket API with connect, disconnect, and sendMessage routes.
Use the SAM Power.

Kiro: [runs sam_init] → creates project structure
[updates template.yaml] → adds AWS::Serverless::WebSocketApi
[creates Lambda handlers] → connect.py, disconnect.py, send_message.py
[runs sam_build] → builds the project
[runs sam_local_invoke] → tests the connect handler locally

The SAM Power also enforces good project structure: separate Lambda handlers in  infrastructure/lambda/ , proper  CodeUri  paths, and  .aws-sam  in your  .gitignore . It gets you from idea to deployed WebSocket API without hand-writing boilerplate.
You can install the SAM Power from the  Kiro Powers marketplace  or add it directly to your project's  .kiro/powers/  directory.

Getting Started

Upgrade  SAM CLI  to get both features:
sam --version
# Need v1.156.0 or later for BuildKit, latest for WebSocket APIs

# Upgrade via pip
pip install --upgrade aws-sam-cli

# Or via Homebrew
brew upgrade aws-sam-cli

For BuildKit, add  --use-buildkit  to your  sam build  command. No template changes needed.
For WebSocket APIs, replace your CloudFormation resources with the new  AWS::Serverless::WebSocketApi  type. If you're starting fresh, the SAM template above is a working starting point.
BuildKit works with  sam local  for local testing. WebSocket APIs currently support  sam deploy  for deployment and  sam local invoke  for testing individual handlers, but full local WebSocket emulation isn't available yet.

Two Features, Zero Breaking Changes

These two features fill gaps that have been open for a while. BuildKit support means  sam build  finally uses the same build engine as the rest of the container ecosystem. WebSocket API support means you can define a real-time API in SAM the same way you define a REST API. A few lines instead of a hundred.
Neither feature changes existing behavior. Both are additive. Upgrade, try them, and keep building.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article