Skip to content

Local Lambda Testing with MiniStack, SAM CLI, and Finch

8 minute read
Content level: Intermediate
2

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 arm64 architecture assumes Apple Silicon (M-series) hardware. If you are running on an x86-based machine, change this to x86_64.

Note: When deploying to AWS, remove or leave the ENDPOINT_URL environment 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.

FeatureMiniStackSAM CLI
LicenseMIT (fully open-source)Apache 2.0
AWS Services Emulated36 (DynamoDB, SQS, SNS, S3, Lambda, etc.)Lambda and API Gateway only
Lambda Runtime EmulationBasic (no bundled boto3)Full (official AWS runtime images)
API Gateway EmulationResource creation only (dispatch not working in v3.0.0)Full (via sam local start-api)
Image Size~200 MBVaries per runtime (~500 MB–1 GB)
RAM at Idle~30 MBN/A (on-demand per invocation)
Startup Time~2 secondsN/A (on-demand per invocation)
Account or API Key RequiredNoNo
TelemetryNoneOptional
Container RuntimeFinch or DockerFinch (v1.145.0+) or Docker
Best ForLightweight local emulation of core AWS servicesLambda-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-out or 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-api instead, 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.yaml under additional_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

AWS
SUPPORT ENGINEER
published a month ago358 views