01
The Problem

Testing cloud functions shouldn't require the cloud

Imagine you've asked an AI to write you a function that processes photos when someone uploads them to your app. The code looks right. But how do you actually test it?

📝

Write code

Ask AI to build a Lambda

🤔

Deploy to AWS

Wait 2+ minutes each time

💸

Test it

Real AWS calls = real costs

🐛

Find a bug

Repeat from step 1

😭

Waste an hour

Just to fix one line

💡

What even is a Lambda function?

An AWS Lambda is a small piece of code that lives in the cloud and runs in response to events — like "a file was uploaded" or "someone hit my API." You write the function, AWS handles all the servers. lambdaforge lets you test that function on your laptop, for free, in seconds.

The solution

lambdaforge runs your Lambda locally

1

Generate a fake trigger event

lambdaforge creates a realistic event JSON file that looks exactly like what AWS would send.

2

Invoke your handler locally

Your handler function runs right on your machine — no cloud needed.

3

Mock AWS services

Calls to S3, DynamoDB, SQS etc. are intercepted by moto — so nothing real happens and nothing costs money.

A real Lambda handler

Code ↔ Plain English

handler.py
import boto3
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    for record in event["Records"]:
        key = record["s3"]["object"]["key"]
        logger.info(f"Got: {key}")
    return {"statusCode": 200}
Import the AWS toolkit (boto3) and Python's logging system
Create a logger so we can print messages that lambdaforge will capture
Set it to show INFO-level messages and above
Define the handler function — this is what AWS (or lambdaforge) calls
Loop through each uploaded file record in the event
Extract the filename from the nested event data
Log it — lambdaforge will capture this and print it for you
Return a 200 OK response to indicate success
Quick check
02
Architecture

Meet the five actors inside lambdaforge

lambdaforge is made of 5 Python files, each with a clear job. Understanding who does what helps you tell AI exactly where to make changes.

🎮

cli.py

The CLI entry point. Reads your flags, validates inputs, and orchestrates everything else. Think of it as the stage manager.

📋

events/

Five files — one per trigger type — each containing a generate() function that returns a realistic fake event dictionary.

invoker.py

The engine. Loads your handler module at runtime, creates a fake AWS context, runs your function, and captures all output.

🎭

mocking.py

Wraps moto's mock_aws() context manager. Intercepts every boto3 call so they never reach the real cloud.

🖨️

output.py

Formats and prints results using Rich — the colored header, captured logs, JSON return value, and timing. This is the last thing that runs.

Live conversation

What happens when you run invoke

Watch the components coordinate in real time:

🧠

Why this matters for steering AI

When you ask AI to "add error handling to my Lambda," you now know to say: "In invoker.py, make invoke_handler() catch exceptions and return a structured error dict instead of re-raising." Precise instructions → better results.

Quick check
03
The generate command

Fake the trigger before you write the code

Every Lambda function is activated by a trigger — something that happened in the world. Before you can test your function, you need a realistic representation of that trigger. That's what generate creates.

$ lambdaforge generate --trigger s3 --output event.json
Event written to event.json

Five trigger types — pick the one that matches your Lambda

🗄️

s3

File uploaded, deleted, or modified in an S3 bucket.

📨

sqs

Message arrived in an SQS queue. Good for background job workers.

📢

sns

Notification published to an SNS topic. Fan-out broadcasting.

🌐

apigateway

HTTP request hit your API Gateway endpoint. Your REST API backend.

eventbridge

A scheduled or custom event from EventBridge. Scheduled tasks and automation.

S3 event — decoded

What's actually inside that JSON file

event.json (generated)
{
  "Records": [
    {
      "eventName": "ObjectCreated:Put",
      "awsRegion": "us-east-1",
      "s3": {
        "bucket": {
          "name": "<your-bucket-name>"
        },
        "object": {
          "key": "<your-object-key>",
          "size": 1024
        }
      }
    }
  ]
}
AWS groups everything into a list of "Records" (could be multiple files)
Each record describes one file event
"Put" means a file was uploaded (vs Deleted, Copied, etc.)
Which AWS region this happened in — edit to match your setup
The S3-specific details live in this nested block
Replace with your real bucket name — e.g. "my-photo-uploads"
Replace with your file path — e.g. "uploads/photo.jpg"
File size in bytes — 1024 = 1KB
Data flow

What generate does step by step

🖥️
CLI
📋
events/s3.py
🏗️
generate()
📄
event.json
event dict
✏️

Always edit the generated file before invoking

Generated events use placeholder values like <your-bucket-name>. Replace them with real values that match what your code expects — the bucket name, file key, or request body your handler will actually process.

Quick check
04
The invoke command

How your handler gets loaded and run

When you run invoke, lambdaforge doesn't just call your function — it creates an entire mini-AWS environment around it in milliseconds.

$ lambdaforge invoke
--handler my_handler.py::process_upload
--event event.json
--mock s3,dynamodb

The invoke pipeline — four stages

1

Validate — before touching your code

CLI checks the handler path format (file.py::func_name), that the file exists, that event.json is valid JSON, and that any requested mock services are in the supported list.

2

Load — importing your handler at runtime

Using Python's importlib, lambdaforge reads your Python file off disk and executes it to find the function. Like hiring a freelancer: you find their portfolio (the file), pick the skill you need (the function name), and hand them the job.

3

Activate mocks — intercept AWS calls

If you passed --mock, a context manager activates moto's mock environment. Every boto3 call your handler makes is intercepted — nothing reaches the real AWS.

4

Run — with captured output

Your function is called with handler(event, context). A fake LambdaContext is passed as the second argument. All print() and logging output is captured, along with execution time. Then it all gets pretty-printed.

invoker.py — decoded

The heart of lambdaforge

lambdaforge/invoker.py
def invoke_handler(handler, event):
    context = LambdaContext()
    captured = io.StringIO()

    log_handler = logging.StreamHandler(captured)
    root_logger = logging.getLogger()
    root_logger.addHandler(log_handler)

    start = time.perf_counter()
    try:
        with redirect_stdout(captured):
            result = handler(event, context)
    finally:
        root_logger.removeHandler(log_handler)

    duration_ms = (time.perf_counter() - start) * 1000
    return result, captured.getvalue(), duration_ms
Define the function that runs your handler
Create a fake AWS context object (function name, request ID, etc.)
Create an in-memory text buffer to capture all output
Wire up Python's logging system to write into that buffer
Get the root logger (parent of all loggers)
Attach our buffer as a logging destination
Record the start time (high-precision)
Try to run (exceptions will still propagate out)
Also redirect print() output into the same buffer
Call your actual handler function — this is the real work
Always clean up the log handler, even if an exception occurred
Calculate elapsed time in milliseconds
Return result, all captured output, and timing
LambdaContext — the fake AWS environment

function_name

"lambdaforge-local" — the pretend function name

aws_request_id

A unique UUID generated fresh each invocation

memory_limit_in_mb

128 MB — matches the Lambda default

get_remaining_time_in_millis()

Always returns 30,000ms — no real timeout locally

Quick check
05
The mocking layer

How fake AWS intercepts real calls

Your Lambda code uses boto3 to talk to S3, DynamoDB, SQS. When you're testing, those calls would go to real AWS — costing money and requiring credentials. Moto stops them before they leave your machine.

🛂

Think of it like a customs checkpoint

Every package (boto3 call) leaving your country (your code) passes through customs (moto). The customs officer inspects it, creates a fake response from their own stockroom, and sends it back — the package never actually crossed the border. Your code gets a response and has no idea it never reached real AWS.

mocking.py — decoded
lambdaforge/mocking.py
from contextlib import contextmanager
from moto import mock_aws

SUPPORTED_SERVICES = {
    "dynamodb", "s3", "ssm",
    "sqs", "sns"
}

@contextmanager
def activated_mocks(services):
    with mock_aws():
        yield
Import the tool that creates "with" blocks
Import moto's main mock activator
The list of services lambdaforge will accept in --mock
Used for validation only — moto intercepts everything regardless
This decorator makes the function usable as a "with" block
Define the function that activates mocking
Turn on moto — ALL boto3 calls are now intercepted
"yield" = run the code inside the "with" block here

Three things you must know about moto

🫙

Empty at start

Moto's fake AWS starts completely empty. Your S3 buckets don't exist yet. Your DynamoDB tables don't exist yet. Your handler must create what it needs — or you create it in a setup step.

🎯

All or nothing

--mock s3,dynamodb doesn't mean only S3 and DynamoDB are mocked — mock_aws() intercepts every boto3 call. The services list is just for display and validation.

💨

Ephemeral

Mocked state is wiped when the "with" block ends. Every invoke run starts fresh — there's no carry-over between invocations. Perfect for test isolation.

Without vs. with mocking

❌ No --mock

boto3 call → real AWS
Needs AWS credentials
Costs money if it runs
Modifies real data!

✓ With --mock s3

boto3 call → moto
No credentials needed
Free, always
Nothing real changes
🔑

The "empty state" pattern

Because moto starts empty, real-world handlers that read from S3 need to either: (1) create the bucket and upload the file in a setup script before invoking, or (2) have your handler gracefully handle a missing resource. This is actually a great way to discover edge cases in your code before production.

Quick check
06
Putting it together

Your complete local testing workflow

You've seen every layer. Here's the full workflow you'd use in a real project — and what to do when things go wrong.

1

Generate a template event

$ lambdaforge generate --trigger s3 --output event.json
2

Edit the JSON to match your real data

Replace <your-bucket-name> with your actual bucket, fill in the file key, adjust the payload.

3

Invoke with mocking enabled

$ lambdaforge invoke --handler handler.py::my_func --event event.json --mock s3,dynamodb
4

Read the output — logs, result, duration

Handler: handler.py::my_func
Mocks: s3, dynamodb
─────────────── Logs ───────────────
[INFO] Processing uploads/photo.jpg
──────────────── Result ────────────
{"statusCode": 200, "count": 1}
Duration: 312ms
5

For richer visual results — use the Python API

As shown in examples/s3_to_dynamo/run_and_display.py: import activated_mocks, load_handler, and invoke_handler directly, then query your mocked services and display with Rich tables — all within the same mock context.

When things break

Common errors and what they mean

⚠️ "handler file not found"

The path in --handler doesn't point to a real file. Check your working directory — lambdaforge resolves paths relative to where you run the command.

⚠️ "invalid handler format"

You need file.py::function_name — two colons, not one. The file path and function name must be separated by ::.

🔴 "NoSuchBucket" from boto3

Your handler is trying to read from an S3 bucket that doesn't exist in the mocked environment. Moto starts empty — create the bucket first, either in your handler's setup or in a run script.

🔴 "ResourceNotFoundException" from DynamoDB

Same issue — your handler is querying a DynamoDB table that hasn't been created in the mock. Add a create_table() call in your handler or a setup step before invoking.

🟣 "unknown service" error

You passed a service name in --mock that lambdaforge doesn't recognize. Supported: s3, dynamodb, sqs, sns, ssm.

🚀

CLI vs. Python API — when to use which

CLI (lambdaforge invoke) — quick iteration, checking if a handler works, viewing logs. Great for a fast feedback loop.

Python API — when you need to query mocked services after the handler runs, build automated tests, or display results visually. Import activated_mocks, load_handler, and invoke_handler from lambdaforge.

Final check

You know how lambdaforge works.

You can read its code, steer AI to modify it precisely, debug failures without guessing, and build Lambda functions with a fast local feedback loop.

⚡ generate → invoke
🎭 moto intercepts boto3
📦 mocked state starts empty
🐍 Python API for advanced use