Deploy and run Python and Node.js functions locally, with no AWS account
When people hear “AWS emulator,” the fair question is always the same: is it actually running my code, or is it just returning a canned response that looks right?
For Lambda in LocalEmu, the answer is that it runs your code, for real, inside the same runtime images AWS uses. In this article I will deploy two functions, in Python and in Node.js, invoke them, and then prove that genuine runtime containers are doing the work. Everything below is actual output from a clean run. No AWS account, no credentials, no cost.
Setup
LocalEmu is a free, open-source AWS cloud emulator. Install it and start it:
pip install "localemu[runtime]"
localemu start
One prerequisite for this walkthrough: Docker must be installed and running, because LocalEmu executes Lambda functions inside real containers. With Docker in place, point the standard AWS CLI at the local endpoint. The clean way is one environment variable that both the AWS CLI and boto3 understand:
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-east-1
The nice part: unset **_AWS\_ENDPOINT\_URL_** and the exact same commands talk to real AWS. Your code does not change.
We need a role ARN for the function. IAM is local too, so this costs nothing:
ROLE_ARN=$(aws iam create-role --role-name lambda-demo \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}' \
--query Role.Arn --output text)
A Python function
Here is a function that doubles a number and reports the Python version it is running on. That second part matters: it lets the function tell us, from the inside, exactly which interpreter is executing it.
# handler.py
import sys
def handler(event, context):
return {
"doubled": int(event.get("x", 0)) * 2,
"runtime": "python " + sys.version.split()[0],
}
Package and deploy it:
zip fn.zip handler.py
aws lambda create-function --function-name doubler-py \
--runtime python3.14 --handler handler.handler \
--role "$ROLE_ARN" --zip-file fileb://fn.zip --timeout 30
aws lambda wait function-active-v2 --function-name doubler-py
Now invoke it. Put the payload in a file and pass it with **_fileb://_**, which sends the bytes as-is. This works the same on AWS CLI v1 and v2; passing — payload ‘{“x”:21}’ as a string is handled differently between the two versions, so the file form avoids that.
cat out.json
Output:
{"doubled": 42, "runtime": "python 3.14.5"}
The function did the arithmetic, and it reported **_python 3.14.5_**. That version string did not come from a mock. It came from a real CPython interpreter running inside the official AWS Lambda Python image.
Proof: a real runtime container
While the function is warm, look at what is running on your Docker host:
docker ps --filter "ancestor=public.ecr.aws/lambda/python:3.14"
Output:
edb5c4d0e600 public.ecr.aws/lambda/python:3.14 "/var/rapid/init" 4 minutes ago Up 4 minutes 0.0.0.0:53814->9563/tcp, [::]:53814->9563/tcp localemu-main-lambda-doubler-py-cdea066067fbec0c1726bc63cbf986e5
That is the official AWS Lambda Python runtime image, pulled from Amazon’s public registry, executing your handler. LocalEmu packaged your zip, started the container, ran your code, and returned the result, the same shape of work AWS does for you in the cloud. Because it is the real runtime, your packaging, your dependencies, your handler signature, and your timeouts all behave the way they will when you deploy.
Same idea, a different language
To show this is not Python-specific, here is the same logic in Node.js:
// index.mjs
export const handler = async (event) => ({
doubled: (event.x ?? 0) * 2,
runtime: "nodejs " + process.version,
});
Deploy and invoke it:
zip fn.zip index.mjs
aws lambda create-function --function-name doubler-node \
--runtime nodejs24.x --handler index.handler \
--role "$ROLE_ARN" --zip-file fileb://fn.zip --timeout 30
aws lambda wait function-active-v2 --function-name doubler-node
aws lambda invoke --function-name doubler-node \
--payload fileb://payload.json out.json
cat out.json
Output:
{"doubled":42,"runtime":"nodejs v24.14.1"}
Node.js 24 itself, reporting v24.14.1 from inside the container. Same workflow, different language, no extra setup.
Why this matters
A mock that returns a plausible JSON body can pass a happy-path test and still hide every interesting bug. Running the real runtime changes that. You find out locally whether your dependencies actually import, whether your handler signature is right, whether your function fits in memory, and how it behaves on a cold start, all before you spend a cent or wait on a deploy. The feedback loop shrinks from minutes to seconds, and you can run it on a plane with no internet.
Where it fits
This is for local development, testing, and learning, not for production. What you get is the loop: write, run real code, see the result, adjust, repeat, at zero cost and with no account. For Lambda, running your code in the official runtime image is exactly the part you most want to be true locally, and it is.
Try it
pip install "localemu[runtime]"
localemu start
Or run the multi-architecture Docker image (Intel and Apple Silicon):
docker run --rm -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock localemu/localemu
Documentation and runnable examples are at https://localemu.cloud. The source is at https://github.com/localemu/localemu, and it is free and open under Apache 2.0.
If it is useful to you, a star on the repository, an issue, or a feature request genuinely helps the project grow. It is maintained in the open, and contributions are welcome.





Top comments (0)