Local Lambda Testing with MiniStack, SAM CLI, and Finch
Set up a complete local Lambda testing environment using MiniStack, SAM CLI, and Finch. Covers DynamoDB and SQS integration, a dual-endpoint pattern for code that works locally and on AWS, a comparison of MiniStack vs SAM CLI, and known issues with workarounds.
Testing AWS Lambda functions locally reduces development cycle time and eliminates AWS costs during iteration. This article walks through setting up a complete local Lambda testing environment using three open-source tools:
- MiniStack — An MIT-licensed AWS service emulator supporting 36 services including DynamoDB, SQS, SNS, and S3. It runs as a lightweight ~200 MB container image with ~30 MB RAM at idle and starts in approximately 2 seconds. No account, API key, or telemetry is required.
- AWS SAM CLI — The official AWS tool for local Lambda invocation and API Gateway emulation. It supports all Lambda runtimes using official AWS runtime images.
- Finch — An open-source container tool from AWS, natively supported by SAM CLI since version 1.145.0.
MiniStack handles AWS service emulation while SAM CLI handles Lambda runtime execution and API Gateway. Together, they provide full local coverage without any AWS spend.
Prerequisites
Before starting, ensure you have the following installed:
- Finch (see Step 1 below)
- AWS CLI v2
- SAM CLI v1.145.0 or later (required for Finch support)
- Python 3.12 or later
Step 1: Install Finch
Install Finch using Homebrew and initialize the virtual machine:
brew install --cask finch finch vm init finch vm start finch --version
For other installation methods, see the Finch GitHub repository.
Step 2: Verify SAM CLI Version
SAM CLI added native Finch support in version 1.145.0. Verify your installed version:
sam --version
If your version is below 1.145.0, upgrade:
pip install --upgrade aws-sam-cli
Note (macOS only): If SAM CLI does not detect Finch automatically, set it as the preferred container runtime:
sudo /usr/libexec/PlistBuddy -c "Add :DefaultContainerRuntime string finch" \ /Library/Preferences/com.amazon.samcli.plist
For full installation details, see Installing Finch for use with SAM CLI.
Step 3: Start MiniStack
You can run MiniStack as a container or install it directly via PyPI.
Container option:
finch run -d --name ministack -p 4566:4566 nahuelnucera/ministack
PyPI option (no container needed):
pip install ministack ministack
After starting MiniStack, verify it is running:
curl -s http://localhost:4566/_localstack/health | python3 -m json.tool
Expected output (truncated):
{ "services": { "s3": "available", "sqs": "available", "dynamodb": "available", "lambda": "available", "iam": "available" }, "edition": "light", "version": "3.0.0.dev" }
Step 4: Create Local Resources in MiniStack
With MiniStack running, create the DynamoDB table and SQS queue that the Lambda functions will use:
aws --endpoint-url=http://localhost:4566 --region us-east-1 \ dynamodb create-table \ --table-name tasks \ --attribute-definitions AttributeName=task_id,AttributeType=S \ --key-schema AttributeName=task_id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST aws --endpoint-url=http://localhost:4566 --region us-east-1 \ sqs create-queue --queue-name task-queue
Step 5: Write Lambda Handlers
The key pattern here is using an ENDPOINT_URL environment variable to control where boto3 sends requests. When the variable is set, boto3 points to MiniStack. When it is absent (on real AWS), boto3 uses the default IAM role. This makes the same code work in both environments without any changes.
CRUD Handler (python-handler/app.py)
import json import os import uuid from datetime import datetime, timezone import boto3 endpoint_url = os.environ.get("ENDPOINT_URL") if endpoint_url: dynamodb = boto3.resource( "dynamodb", endpoint_url=endpoint_url, region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"), aws_access_key_id="test", aws_secret_access_key="test", ) else: dynamodb = boto3.resource("dynamodb") table = dynamodb.Table(os.environ.get("TABLE_NAME", "tasks")) def handler(event, context): try: body = json.loads(event.get("body", "{}")) except (json.JSONDecodeError, TypeError): return {"statusCode": 400, "body": json.dumps({"error": "Invalid JSON"})} title = body.get("title") if not title: return {"statusCode": 400, "body": json.dumps({"error": "title is required"})} task = { "task_id": str(uuid.uuid4()), "title": title, "description": body.get("description", ""), "status": "pending", "created_at": datetime.now(timezone.utc).isoformat(), } table.put_item(Item=task) return { "statusCode": 201, "headers": {"Content-Type": "application/json"}, "body": json.dumps(task), }
SQS Processor Handler (sqs-handler/app.py)
This handler demonstrates processing messages from an SQS queue and writing results to DynamoDB, using the same dual-endpoint pattern:
import json import os import uuid from datetime import datetime, timezone import boto3 endpoint_url = os.environ.get("ENDPOINT_URL") if endpoint_url: dynamodb = boto3.resource( "dynamodb", endpoint_url=endpoint_url, region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"), aws_access_key_id="test", aws_secret_access_key="test", ) else: dynamodb = boto3.resource("dynamodb") table = dynamodb.Table(os.environ.get("TABLE_NAME", "tasks")) def handler(event, context): results = [] for record in event.get("Records", []): try: body = json.loads(record.get("body", "{}")) except (json.JSONDecodeError, TypeError): continue task = { "task_id": str(uuid.uuid4()), "title": body.get("title", "Untitled"), "description": body.get("description", ""), "status": "pending", "source": "sqs", "created_at": datetime.now(timezone.utc).isoformat(), } table.put_item(Item=task) results.append(task["task_id"]) return {"statusCode": 200, "body": json.dumps({"processed": len(results)})}
Step 6: Create the SAM Template
Create a template.yaml file that defines both Lambda functions and an HTTP API:
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Local Lambda testing with MiniStack and SAM CLI Globals: Function: Timeout: 30 Runtime: python3.12 Architectures: - arm64 Environment: Variables: TABLE_NAME: tasks ENDPOINT_URL: http://host.docker.internal:4566 AWS_DEFAULT_REGION: us-east-1 Resources: TasksApi: Type: AWS::Serverless::HttpApi Properties: StageName: prod TaskCrudFunction: Type: AWS::Serverless::Function Properties: Handler: app.handler CodeUri: python-handler/ Events: CreateTask: Type: HttpApi Properties: ApiId: !Ref TasksApi Path: /tasks Method: POST TaskProcessorFunction: Type: AWS::Serverless::Function Properties: Handler: app.handler CodeUri: sqs-handler/
The ENDPOINT_URL uses host.docker.internal because SAM CLI runs Lambda inside a Finch container, and the container needs to reach MiniStack on the host machine.
Note: The
arm64architecture assumes Apple Silicon (M-series) hardware. If you are running on an x86-based machine, change this tox86_64.
Note: When deploying to AWS, remove or leave the
ENDPOINT_URLenvironment variable empty so that boto3 uses the default AWS endpoints with your IAM role.
Step 7: Test End-to-End with SAM CLI
Start the local API Gateway:
sam local start-api --port 3000
In another terminal, send a request:
curl -X POST http://127.0.0.1:3000/tasks \ -H "Content-Type: application/json" \ -d '{"title": "API Gateway via SAM", "description": "End-to-end SAM and MiniStack"}'
Example output:
{ "task_id": "f8153d5e-8b8d-4abc-9d5d-db50cdf18529", "title": "API Gateway via SAM", "description": "End-to-end SAM and MiniStack", "status": "pending", "created_at": "2026-04-03T19:05:48.406356+00:00" }
Verify the item landed in MiniStack DynamoDB:
aws --endpoint-url=http://localhost:4566 --region us-east-1 \ dynamodb scan --table-name tasks \ --query 'Items[?title.S==`API Gateway via SAM`]'
The full request flow is: curl → SAM CLI API Gateway (port 3000) → Lambda container (real Python 3.12 runtime) → boto3 → MiniStack DynamoDB (port 4566).
Comparison: MiniStack vs SAM CLI
Understanding which tool handles what helps you decide the right combination for your use case.
| Feature | MiniStack | SAM CLI |
|---|---|---|
| License | MIT (fully open-source) | Apache 2.0 |
| AWS Services Emulated | 36 (DynamoDB, SQS, SNS, S3, Lambda, etc.) | Lambda and API Gateway only |
| Lambda Runtime Emulation | Basic (no bundled boto3) | Full (official AWS runtime images) |
| API Gateway Emulation | Resource creation only (dispatch not working in v3.0.0) | Full (via sam local start-api) |
| Image Size | ~200 MB | Varies per runtime (~500 MB–1 GB) |
| RAM at Idle | ~30 MB | N/A (on-demand per invocation) |
| Startup Time | ~2 seconds | N/A (on-demand per invocation) |
| Account or API Key Required | No | No |
| Telemetry | None | Optional |
| Container Runtime | Finch or Docker | Finch (v1.145.0+) or Docker |
| Best For | Lightweight local emulation of core AWS services | Lambda-specific testing with real runtime parity |
MiniStack and SAM CLI complement each other well. MiniStack handles AWS service emulation (DynamoDB, SQS, S3, and others) while SAM CLI handles Lambda runtime execution with official AWS images. Together, they cover the most common local testing scenarios at zero cost.
Known Issues and Workarounds
-
MiniStack does not bundle boto3. Unlike the real AWS Lambda Python runtime, MiniStack does not include boto3. If deploying Lambda directly to MiniStack (without SAM CLI), you must package boto3 in your deployment zip. SAM CLI does not have this issue because it uses the official Lambda runtime image.
-
AWS CLI v2 base64 payload. When invoking Lambda via
aws lambda invoke, add--cli-binary-format raw-in-base64-outor the payload is treated as base64, causing the invocation to fail with "Invalid base64." -
MiniStack API Gateway dispatch. API Gateway resource creation works in MiniStack, but HTTP request dispatch to Lambda does not work in MiniStack v3.0.0. Use SAM CLI
sam local start-apiinstead, as shown in Step 7. -
Finch directory mounting. On macOS, SAM CLI local commands fail if your project is outside your home directory (
~) or/Volumes. Move your project to your home directory or add the path to~/.finch/finch.yamlunderadditional_directories. -
SAM CLI version requirement. Finch support requires SAM CLI v1.145.0 or later. Earlier versions only detect Docker and fail with "Running AWS SAM projects locally requires Docker."
Cleanup
Stop and remove the MiniStack container when you are done:
finch stop ministack && finch rm ministack
Sources
- Topics
- ServerlessCompute
- Language
- English
Amazing, thanks for this!! Just a heads-up, starting from v1.2.9, we’re bundling the AWS CLI, so the image size has increased to 269MB. It’s a trade-off, but it improves capabilities. The image can now be pulled from ministackorg/ministack. :D
Relevant content
AWS OFFICIALUpdated 2 years ago