# Test a Serverless Worker locally with LocalStack

> **Pre-release**
> APIs are experimental and may be subject to backwards-incompatible changes.

Run the full [Serverless Worker](/serverless-workers) loop on your machine with no AWS account and no Temporal Cloud.
[LocalStack](https://www.localstack.cloud/) emulates AWS Lambda locally, and the
[Temporal development server](/develop/run-a-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](/production-deployment/worker-deployments/serverless-workers/aws-lambda).

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.

> **📝 Note:**
> 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](/production-deployment/worker-deployments/serverless-workers/aws-lambda).
>

## Prerequisites 

- [Docker](https://docs.docker.com/get-docker/).
- The [Temporal CLI](/cli), which includes the development server.
- The [`aws` CLI](https://aws.amazon.com/cli/), or [`awslocal`](https://github.com/localstack/awscli-local), a thin
  wrapper that targets LocalStack automatically.
- A Worker packaged as a Lambda zip. Follow
  [Write Worker code](/production-deployment/worker-deployments/serverless-workers/aws-lambda#write-worker-code) and
  [Build and package](/production-deployment/worker-deployments/serverless-workers/aws-lambda#build-and-package) in the
  AWS Lambda guide, or start from a sample:
  [Go](https://github.com/temporalio/samples-go/tree/main/lambda-worker),
  [Python](https://github.com/temporalio/samples-python/tree/main/lambda_worker),
  [TypeScript](https://github.com/temporalio/samples-typescript/tree/main/lambda-worker).

## 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.

```bash
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"
```

> **📝 Note:**
> Choose a LocalStack version
>
> Pin a released [Community tag](https://github.com/localstack/localstack/releases) 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](https://docs.localstack.cloud/user-guide/aws/lambda/).
>

## 2. Start the Temporal development server 

The development server is a self-hosted Temporal Service, so enable the [Worker Controller](/serverless-workers#how-invocation-works)
the same way you would for [self-hosted setup](/production-deployment/worker-deployments/serverless-workers/self-hosted-setup#enable-worker-controller).
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:

```bash
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`.

> **📝 Note:**
>
> 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](/production-deployment/worker-deployments/serverless-workers/self-hosted-setup#enable-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.

```bash
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](/production-deployment/worker-deployments/serverless-workers/aws-lambda#deploy-lambda-function-step)
in the AWS Lambda guide. The example values are for the
[Python sample](https://github.com/temporalio/samples-python/tree/main/lambda_worker). 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](/serverless-workers#worker-lifecycle).
>

## 4. Create the Worker Deployment Version 

Create the Worker Deployment Version exactly as in the AWS Lambda guide's
[Create Worker Deployment Version](/production-deployment/worker-deployments/serverless-workers/aws-lambda#create-worker-deployment-version)
and [Set version as current](/production-deployment/worker-deployments/serverless-workers/aws-lambda#set-current-version)
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.

```bash
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.

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

Verify the run:

```bash
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:ExternalId` trust condition, deploy to a real AWS account with the
  [AWS Lambda deployment guide](/production-deployment/worker-deployments/serverless-workers/aws-lambda).
- To configure a self-hosted Temporal Service to invoke Lambda functions in AWS, see
  [Self-hosted setup](/production-deployment/worker-deployments/serverless-workers/self-hosted-setup).
- If a Workflow does not progress or the Lambda is not invoked, see
  [Troubleshoot Serverless Workers](/troubleshooting/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.                            |
