Building serverless functions with Python, Node, Java, .NET 10, and Go runtimes, mastering create-function and invoke, understanding cold start and hot reload, then triggering Lambda from S3 and SQS events.

In episode 6 we dissected DynamoDB as a datastore, and in episode 5 S3 as an object store. This time we get into the brain of serverless architecture: AWS Lambda. Lambda is the backbone of almost every event-driven pattern on AWS — from processing file uploads and reading message queues to serving API Gateway requests. That's why understanding how Lambda works in LocalStack is the most valuable skill in this series.
In this episode you'll build functions from scratch, run them across five different runtimes, dissect cold start behavior, and connect them to S3 and SQS triggers.
Before deploying, you need handler code. Python Lambda expects a function with the signature (event, context) that returns a JSON-serializable value:
def handler(event, context):
print("event diterima:", event)
return {
"statusCode": 200,
"body": "Halo dari LocalStack Lambda!"
}Zip the handler folder first, then deploy:
zip -r function.zip handler.py
awslocal lambda create-function \
--function-name hello \
--runtime python3.12 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler \
--zip-file fileb://function.zipThe role used is a fictitious ARN. LocalStack doesn't validate IAM as strictly as real AWS — the goal is for commands to look identical to production. The destination endpoint remains http://localhost:4566 because all commands are sent through awslocal, the wrapper of aws --endpoint-url.
awslocal lambda invoke --function-name hello \
--payload '{"nama": "Arman"}' output.json
cat output.jsonLocalStack genuinely executes the function inside a container, not just mocks it. To trace its internal process, run localstack logs while triggering an invoke.
One of LocalStack's strengths is runtime parity: you can deploy code for any language stack without downloading a special emulator:
| Runtime | Runtime Value | Notes |
|---|---|---|
| Python | python3.12 / python3.13 | Fastest for prototyping |
| Node.js | nodejs22.x | Popular runtime for APIs |
| Java | java21 | Requires building a JAR/zip |
| .NET 10 | dotnet10 | Newest runtime, supported since 2026.x |
| Go | provided.al2023 | Compile a binary, use a custom runtime |
The command is exactly the same — just change the --runtime flag and handler. For Go, the handler points to the executable binary, not a function name:
GOOS=linux GOARCH=amd64 go build -o bootstrap main.go
zip -r go-function.zip bootstrap
awslocal lambda create-function \
--function-name go-fn --runtime provided.al2023 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler bootstrap --zip-file fileb://go-function.zipWhen a function is invoked after a long idle period, the runtime must be "started up" first: pull the image, initialize the runtime, then run the handler. This is called a cold start and adds latency of up to several seconds. Subsequent calls use a warm instance and are therefore much faster. In LocalStack, the first invoke is almost always cold — this teaches you to write code that doesn't assume state between invokes.
Redeploying with a ZIP every time you change a single line is slow. For a fast dev loop, LocalStack provides hot reload via volume mount. Run the container with the LAMBDA_MOUNT_CWD env var pointing to your code folder:
services:
localstack:
image: localstack/localstack:latest
ports:
- "4566:4566"
environment:
- LAMBDA_MOUNT_CWD=/code
volumes:
- ./functions:/codeNow edit functions/handler.py, and the function pointing to that folder will immediately use the latest code without update-function-code. This is the pattern teams use for fast iteration.
Warning
Hot reload only refreshes the mounted code. Changes to runtime, environment variables, or the handler still require awslocal lambda update-function-configuration.
Every function needs configuration like a table name or log level. Send it via --environment:
awslocal lambda create-function \
--function-name config-fn --runtime python3.12 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler --zip-file fileb://function.zip \
--environment 'Variables={ENV=dev,LOG_LEVEL=debug}'Inside the handler, read them via os.environ.get("ENV") as usual — the syntax is identical to real AWS.
Layers share dependencies across functions (e.g. extra boto3 libraries or internal utilities). Publish a layer once, then attach it to many functions:
zip -r layer.zip python/
awslocal lambda publish-layer-version \
--layer-name deps --zip-file fileb://layer.zip
awslocal lambda create-function \
--function-name with-layer --runtime python3.12 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler --zip-file fileb://function.zip \
--layers "arn:aws:lambda:us-east-1:000000000000:layer:deps:1"Lambda is rarely invoked manually; it's run by events. The two most common triggers are S3 (object created/deleted) and SQS (incoming message).
awslocal s3 mb s3://uploads
awslocal lambda create-function \
--function-name resize --runtime python3.12 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler --zip-file fileb://function.zip
awslocal s3api put-bucket-notification-configuration \
--bucket uploads --notification-configuration \
'{"LambdaFunctionConfigurations":[{"LambdaFunctionArn":"arn:aws:lambda:us-east-1:000000000000:function:resize","Events":["s3:ObjectCreated:*"]}]}'
awslocal s3 cp image.jpg s3://uploads/image.jpgFor SQS, use an event source mapping. LocalStack pulls messages from the queue and invokes the function automatically:
awslocal sqs create-queue --queue-name jobs
awslocal lambda create-event-source-mapping \
--function-name worker \
--event-source-arn arn:aws:sqs:us-east-1:000000000000:jobs
awslocal sqs send-message --queue-url http://localhost:4566/000000000000/jobs \
--message-body '{"id": 42}'Check the function logs with localstack logs — you'll see the incoming message payload as the event.
Tip
Be precise with the ARN in event source mapping: the format is arn:aws:sqs:<region>:<account>:<queue-name>. A wrong account or region means the mapping will never trigger an invoke.
Summary of this episode:
awslocal lambda create-function and run it with awslocal lambda invoke; a fictitious IAM role is enough for emulation.provided.al2023.LAMBDA_MOUNT_CWD for hot reload.Lambda is now running in your emulator. But functions called one by one aren't very useful — event-driven architectures need a message pipeline. In episode 8 we dissect SQS and SNS: creating queues and topics, sending and receiving messages, dead-letter queues, visibility timeout, and fan-out patterns. See you there!