Skip to main content

Test a Serverless Worker locally with LocalStack

View Markdown

Run the full Serverless Worker loop on your machine with no AWS account and no Temporal Cloud. LocalStack emulates AWS Lambda locally, and the Temporal development server stands in for a Temporal Service. Use this to evaluate Serverless Workers or to iterate on Worker code before you deploy to AWS.

The flow is the same as a real deployment, with two differences: you deploy the Lambda to the LocalStack endpoint, and the Worker reaches the development server through host.docker.internal instead of a public address.

What local testing does and does not cover

LocalStack validates your Worker code, packaging, and the full invoke-poll-complete loop driven by the Worker Controller. It does not validate real AWS IAM role assumption or the aws:ExternalId trust condition, because LocalStack's IAM is permissive. Validate those against a real AWS account with the AWS Lambda deployment guide.

Prerequisites

1. Start LocalStack

Start the LocalStack Community edition with the Lambda, STS, and IAM services. Mount the Docker socket so LocalStack can run your Lambda in a container.

docker run --rm -d --name localstack -p 4566:4566 \
-e SERVICES=lambda,sts,iam \
-v /var/run/docker.sock:/var/run/docker.sock \
localstack/localstack:<version>

# Wait until the Lambda service is ready.
until curl -s http://localhost:4566/_localstack/health | grep -q '"lambda"'; do sleep 1; done
echo "LocalStack ready"
Choose a LocalStack version

Pin a released Community tag that supports your Lambda runtime. Avoid :latest, which may try to activate the Pro edition and refuse to start without a token. The Lambda runtime you can deploy depends on your LocalStack version: older releases such as 3.8.1 support up to python3.12, while recent 4.x releases add python3.13. Confirm your version supports the runtime you deploy in the LocalStack Lambda docs.

2. Start the Temporal development server

The development server is a self-hosted Temporal Service, so enable the Worker Controller the same way you would for self-hosted setup. The Worker Controller runs inside the server and calls the compute provider, so point it at LocalStack with AWS_ENDPOINT_URL and placeholder credentials. The development server has no dynamic configuration file, so pass the Worker Controller settings as --dynamic-config-value flags:

AWS_ENDPOINT_URL=http://localhost:4566 \
AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_REGION=us-east-1 \
temporal server start-dev --ip 0.0.0.0 \
--dynamic-config-value workercontroller.enabled=true \
--dynamic-config-value workercontroller.compute_providers.aws.require_role_and_external_id=false
SettingWhy it is needed for local development
AWS_ENDPOINT_URL and AWS_* credentialsPoint the server's Worker Controller at LocalStack (http://localhost:4566) instead of real AWS, so it invokes your LocalStack function. LocalStack accepts the placeholder test credentials.
--ip 0.0.0.0Binds the server to all network interfaces so the Lambda container can reach it. The default 127.0.0.1 is not reachable from the container. The Worker connects back through host.docker.internal:7233.
workercontroller.enabled=trueTurns on the Worker Controller, which is disabled by default. This is the same setting used in self-hosted setup.
workercontroller.compute_providers.aws.require_role_and_external_id=falseRelaxes the AWS role and External ID check. LocalStack's IAM cannot enforce the aws:ExternalId trust condition, so skip it locally and validate the real path on AWS.

The Web UI is at http://localhost:8233.

info

This command sets the minimum needed for local use. If Tasks are not picked up, add the compute_providers.enabled and scaling_algorithms.enabled settings from Enable the Worker Controller as additional --dynamic-config-value flags with JSON list values, for example --dynamic-config-value 'workercontroller.scaling_algorithms.enabled=["no-sync"]'.

3. Deploy the Worker to LocalStack

Deploy the packaged Worker as a normal Lambda, but point the aws CLI at the LocalStack endpoint and set TEMPORAL_ADDRESS so the Worker connects back to the development server on your host. LocalStack's IAM is permissive, so the --role value is a placeholder.

export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1

aws lambda create-function --endpoint-url http://localhost:4566 \
--function-name my-temporal-worker \
--runtime python3.13 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::000000000000:role/dev \
--zip-file fileb://function.zip \
--timeout 600 \
--memory-size 256 \
--environment '{"Variables":{"TEMPORAL_ADDRESS":"host.docker.internal:7233","TEMPORAL_NAMESPACE":"default"}}'

Set --runtime and --handler to match how you packaged the Worker, as described in Deploy Lambda function in the AWS Lambda guide. The example values are for the Python sample. Only two things differ from a real deployment: the --endpoint-url targets LocalStack, and TEMPORAL_ADDRESS is host.docker.internal:7233.

caution

AWS Lambda defaults to a 3-second timeout, which is too short for the Worker to start, connect, and register its Task Queue. Set --timeout high enough for the Worker to start, process Tasks, and shut down gracefully.

4. Create the Worker Deployment Version

Create the Worker Deployment Version exactly as in the AWS Lambda guide's Create Worker Deployment Version and Set version as current steps, but use the LocalStack function ARN. Omit the assume-role ARN and External ID: with require_role_and_external_id=false, the server accepts an ARN-only configuration. Passing placeholder values instead fails, because the server cannot verify the aws:ExternalId trust condition against LocalStack's permissive IAM.

export TEMPORAL_ADDRESS=localhost:7233

# Create the Worker Deployment first; create-version requires it to exist.
temporal worker deployment create --name my-app

temporal worker deployment create-version \
--deployment-name my-app \
--build-id build-1 \
--aws-lambda-function-arn arn:aws:lambda:us-east-1:000000000000:function:my-temporal-worker

temporal worker deployment set-current-version \
--deployment-name my-app \
--build-id build-1

The --deployment-name and --build-id must match the values in your Worker code.

5. Run a Workflow

Start a Workflow on the Task Queue. When the Task lands with no Worker polling, the Worker Controller invokes your LocalStack function; the Worker starts, connects back, polls, and completes the Workflow.

temporal workflow start \
--task-queue my-task-queue \
--type MyWorkflow \
--input '"Hello, serverless!"'

Verify the run:

docker logs localstack | grep lambda.Invoke # a Worker Controller-driven invocation
temporal workflow list # the Workflow reaches Completed

You can also watch the Workflow reach Completed in the Web UI at http://localhost:8233.

Next steps

Troubleshooting

SymptomFix
The Worker cannot reach the server.Start the development server with --ip 0.0.0.0 and set TEMPORAL_ADDRESS=host.docker.internal:7233 on the Lambda.
create-function rejects the runtime as unsupported.Set --runtime to a value your LocalStack version supports, or upgrade to a newer LocalStack version.
LocalStack does not start or asks for a token.Do not use the :latest tag on the Community image. Pin a released Community tag instead.
The first invocation times out before the Worker polls.Increase --timeout on the Lambda so the Worker has time to start, register its Task Queue, and shut down gracefully.
create-version fails with ... does not require the configured external ID.You passed --aws-lambda-assume-role-* against LocalStack. Omit both; with require_role_and_external_id=false, the server accepts an ARN-only configuration.