Test a Serverless Worker locally with LocalStack
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.
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
- Docker.
- The Temporal CLI, which includes the development server.
- The
awsCLI, orawslocal, a thin wrapper that targets LocalStack automatically. - A Worker packaged as a Lambda zip. Follow Write Worker code and Build and package in the AWS Lambda guide, or start from a sample: Go, Python, TypeScript.
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"
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
| Setting | Why it is needed for local development |
|---|---|
AWS_ENDPOINT_URL and AWS_* credentials | Point 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.0 | Binds 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=true | Turns 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=false | Relaxes 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.
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.
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
- To validate real IAM role assumption and the
aws:ExternalIdtrust condition, deploy to a real AWS account with the AWS Lambda deployment guide. - To configure a self-hosted Temporal Service to invoke Lambda functions in AWS, see Self-hosted setup.
- If a Workflow does not progress or the Lambda is not invoked, see Troubleshoot Serverless Workers.
Troubleshooting
| Symptom | Fix |
|---|---|
| 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. |