How to test Lambda locally: the full guide for 2026

April 22, 2026 ยท Lucas Vieira

If you want to run AWS Lambda locally, you have a few options, and most of them are worse than they need to be.

This guide shows how to test Lambda locally end-to-end: real function code executing in a real runtime, triggered by real events from other AWS services, asserting on real side effects. No account, no auth token, no paid tier.

We'll use fakecloud โ€” a free, open-source AWS emulator that runs Lambda in real Docker containers across all 13 official runtimes and wires it up to 22 other AWS services that trigger and consume it.

What you need

Install fakecloud

curl -fsSL https://raw.githubusercontent.com/faiscadev/fakecloud/main/install.sh | bash
fakecloud

That's it. It listens on http://localhost:4566.

If you prefer Docker:

docker run --rm \
  -p 4566:4566 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  ghcr.io/faiscadev/fakecloud

The Docker socket mount is only needed because Lambda execution needs Docker-in-Docker. The single-binary install above doesn't need it.

A Node.js Lambda, end-to-end

Create the function:

cat > index.js <<'EOF'
exports.handler = async (event) => {
  return { statusCode: 200, body: JSON.stringify({ event }) };
};
EOF
zip fn.zip index.js

Deploy it to fakecloud:

aws --endpoint-url http://localhost:4566 iam create-role \
  --role-name lambda-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'

aws --endpoint-url http://localhost:4566 lambda create-function \
  --function-name hello \
  --runtime nodejs20.x \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --handler index.handler \
  --zip-file fileb://fn.zip

Invoke it:

aws --endpoint-url http://localhost:4566 lambda invoke \
  --function-name hello \
  --payload '{"hello":"world"}' \
  --cli-binary-format raw-in-base64-out \
  out.json
cat out.json

Output:

{"statusCode":200,"body":"{\"event\":{\"hello\":\"world\"}}"}

That's your Node.js code executing in a real Node 20 container. fakecloud pulled the runtime image, mounted your zip, and invoked handler.

Python, Java, Go, .NET, Ruby, custom

Same flow, different runtime string. fakecloud supports all 13 AWS Lambda runtimes:

Example โ€” Python:

cat > lambda_function.py <<'EOF'
def handler(event, context):
    return {"ok": True, "event": event}
EOF
zip fn.zip lambda_function.py

aws --endpoint-url http://localhost:4566 lambda create-function \
  --function-name py-hello \
  --runtime python3.12 \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --handler lambda_function.handler \
  --zip-file fileb://fn.zip

aws --endpoint-url http://localhost:4566 lambda invoke \
  --function-name py-hello \
  --payload '{"x":1}' \
  --cli-binary-format raw-in-base64-out \
  out.json

Triggers that actually fire

This is where "test Lambda locally" usually breaks down. Your Lambda is not invoked by a human calling Invoke โ€” it's triggered by S3, SQS, SNS, EventBridge, DynamoDB Streams, API Gateway, or an event source mapping. fakecloud has those wired up for real.

SQS event source mapping

aws --endpoint-url http://localhost:4566 sqs create-queue --queue-name jobs

QUEUE_URL=http://localhost:4566/000000000000/jobs
QUEUE_ARN=arn:aws:sqs:us-east-1:000000000000:jobs

aws --endpoint-url http://localhost:4566 lambda create-event-source-mapping \
  --function-name hello \
  --event-source-arn $QUEUE_ARN \
  --batch-size 1

aws --endpoint-url http://localhost:4566 sqs send-message \
  --queue-url $QUEUE_URL \
  --message-body '{"job":"resize","id":42}'

fakecloud polls the queue, batches the message, invokes your Lambda with the SQS event shape, and deletes the message on success. Same contract as real AWS.

S3 -> Lambda

aws --endpoint-url http://localhost:4566 s3 mb s3://uploads
aws --endpoint-url http://localhost:4566 lambda add-permission \
  --function-name hello \
  --statement-id s3invoke \
  --action lambda:InvokeFunction \
  --principal s3.amazonaws.com \
  --source-arn arn:aws:s3:::uploads

aws --endpoint-url http://localhost:4566 s3api put-bucket-notification-configuration \
  --bucket uploads \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [{
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:hello",
      "Events": ["s3:ObjectCreated:*"]
    }]
  }'

echo "hi" | aws --endpoint-url http://localhost:4566 s3 cp - s3://uploads/file.txt

Your Lambda fires with the S3 event. End-to-end, no stubs.

EventBridge -> Lambda

aws --endpoint-url http://localhost:4566 events put-rule \
  --name on-order \
  --event-pattern '{"source":["store"],"detail-type":["OrderPlaced"]}'

aws --endpoint-url http://localhost:4566 events put-targets \
  --rule on-order \
  --targets 'Id=1,Arn=arn:aws:lambda:us-east-1:000000000000:function:hello'

aws --endpoint-url http://localhost:4566 events put-events \
  --entries 'Source=store,DetailType=OrderPlaced,Detail={"orderId":"o1"}'

Your Lambda fires with the EventBridge event envelope.

Asserting on side effects

fakecloud ships test-assertion SDKs that let your tests check what happened without raw HTTP:

import { FakeCloud } from "fakecloud";
const fc = new FakeCloud();

// Your app publishes to SNS inside a Lambda. Your test asserts it happened.
const { invocations } = await fc.lambda.getInvocations({ functionName: "hello" });
expect(invocations).toHaveLength(1);
expect(invocations[0].statusCode).toBe(200);

const { messages } = await fc.sns.getPublishedMessages({ topicName: "orders" });
expect(messages[0].message).toContain("o1");

await fc.reset();

SDKs in TypeScript, Python, Go, PHP, Java, Rust. Reference: fakecloud.dev/docs/sdks.

Watching the logs

Lambda stdout goes to CloudWatch Logs, which fakecloud also emulates:

aws --endpoint-url http://localhost:4566 logs tail /aws/lambda/hello --follow

Running in CI

# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSL https://raw.githubusercontent.com/faiscadev/fakecloud/main/install.sh | bash
      - run: fakecloud &
      - run: |
          for i in $(seq 1 30); do
            curl -sf http://localhost:4566/_fakecloud/health && break
            sleep 1
          done
      - run: npm ci && npm test
        env:
          AWS_ENDPOINT_URL: http://localhost:4566
          AWS_ACCESS_KEY_ID: test
          AWS_SECRET_ACCESS_KEY: test
          AWS_REGION: us-east-1

~500ms startup. A whole Lambda-integration test suite completes in seconds on a cold runner.

Correctness vs performance

fakecloud runs your function code in the real AWS Lambda runtime containers, so behavior matches real Lambda. What it doesn't replicate is AWS's distributed infrastructure timing: cold-start latency, concurrency-accounting scheduling, and VPC networking come from AWS's own implementation, not from a local process, so numbers on those will not match real AWS. Use real AWS for those measurements, use fakecloud for everything correctness-related.