Where we're going, we don't NEED workers!
A live audience-participation demo for Temporal's Serverless Workers feature. Attendees submit their name via a web UI to trigger real workflow executions, and watch Lambda invocations, task queue backlog, and workflow counts update in real time.
temporal-serverless-no-roads/
├── shared/ # Shared Go module — workflows, activities, task queue, worker config
│ ├── activities/ # ProcessSubmission + SimulateWork1/2/3 activity implementations
│ ├── taskqueue/ # Shared task queue name constant
│ ├── workerconfig/ # Reads worker concurrency from environment variables
│ │ └── .env # Worker concurrency settings for local dev/to invoke for the Lambda
│ └── workflows/ # DemoWorkflow definition
├── lambda-worker/ # Deployable 1: Lambda worker (Go)
│ ├── cfn/
│ │ └── execution-role.yaml # CloudFormation: Lambda execution role (CloudWatch + Secrets Manager)
│ ├── deploy-lambda.sh # Build, package, create-or-update Lambda function
│ ├── Makefile # Orchestrates cfn-execution-role → deploy
├── demo-app/ # Deployable 2: HTTP server — UI + API (Go)
│ ├── api/ # /api/submit, /api/metrics, /api/seed handlers
│ ├── cache/ # Short-TTL metrics cache
│ ├── frontend/ # Embedded HTML UI (served at /)
│ ├── localworker/ # Long-polling worker for local dev (not deployed to Lambda)
│ ├── middleware/ # Per-IP rate limiter
│ └── k8s/ # Kubernetes manifests for EKS deployment
├── go.work # Go workspace — ties all three modules together
└── README.md
Audience browsers submit their name via the demo UI, which is served by the
demo app running on EKS. The submit handler starts a DemoWorkflow via the
Temporal Go SDK client. Temporal Cloud receives it, routes it to the
serverless-webinar task queue, and the WCI scaler monitors backlog depth —
invoking the Lambda worker when tasks need processing. The Lambda worker polls
the task queue, executes DemoWorkflow (three chained SimulateWork
activities with built in delays to show execution over time), and exits when idle. The metrics handler polls Temporal Cloud for
workflow counts and task queue stats to drive the live dashboard.
See METRICS.md for a full breakdown of where each UI metric comes from, which Temporal API backs it, and the end-to-end lag from a real-world event to the number changing on screen.
Local dev uses the Temporal CLI dev server in place of Temporal Cloud, and a standard long-polling worker in place of the Lambda worker. No AWS account or Temporal Cloud namespace required.
# macOS
brew install temporal
# Linux — see https://temporal.download/cli/archive/latest?platform=linux&arch=amd64Run go mod tidy in each module. The workspace root has no go.mod, so this
must be done per-module:
cd shared && go mod tidy && cd ..
cd lambda-worker && go mod tidy && cd ..
cd demo-app && go get go.temporal.io/api && go mod tidy && cd ..
go work sync
go.temporal.io/apineeds an explicit entry indemo-app/go.modbecausemetrics.goimportsgo.temporal.io/api/workflowservice/v1directly forCountWorkflowExecutionsRequest.
temporal server start-devStarts a local Temporal cluster at localhost:7233. Web UI at
http://localhost:8233. No auth, no TLS.
The Lambda worker uses lambdaworker.RunWorker, which exits after each task
batch and is not suited for local iteration. Use the long-polling local worker
instead:
cd demo-app
go run ./localworker/main.goConcurrency is configured via demo-app/localworker/.env. The defaults (5
concurrent activities, 5 workflow tasks) are intentionally low to simulate
single-Lambda-invocation capacity, making backlog depth and sync match rate
pressure visible with a realistic seed count. To override for one run:
WORKER_MAX_CONCURRENT_ACTIVITIES=2 go run ./localworker/main.gocd demo-app
go run .The demo app is available at http://localhost:8080.
Open http://localhost:8080, enter a name, click Start workflow, and watch the dashboard update. To saturate the worker and produce visible backlog and sync match rate pressure, fire a seed burst:
curl -X POST http://localhost:8080/api/seed?count=30Or activate presenter mode in the UI at ?presenter=1 in the URL.
| Terminal | Command | Purpose |
|---|---|---|
| 1 | temporal server start-dev |
Local Temporal cluster + Web UI |
| 2 | cd demo-app && go run ./localworker/main.go |
Long-polling worker |
| 3 | cd demo-app && go run . |
Demo app server |
This section covers the complete end-to-end process for deploying the Lambda worker to the SA AWS account and connecting it to Temporal Cloud.
Namespace auth requirement: This repo assumes that your Temporal Cloud namespace is configured for API key authentication.
-
A Temporal Cloud namespace with API key. Download them from the Temporal Cloud UI under your namespace → API Keys.
-
Temporal CLI installed (
brew install temporal).
Store your API key so the Lambda can fetch it at cold start without it being visible in plain text as an environment variable.
aws secretsmanager create-secret \
--name temporal/serverless-webinar/api-key \
--secret-string "<your-temporal-api-key>" \
--region <your-region> \
--profile <your-profile>To update the secret on subsequent runs, use
put-secret-valueinstead ofcreate-secret.Simpler alternative for demos: skip this step and set
TEMPORAL_API_KEYdirectly as a Lambda environment variable in Step 4. The key is visible in the Lambda console, which is fine for a short-lived demo but not for production.
Creates the IAM execution role the Lambda function assumes at runtime. Grants CloudWatch Logs write access and Secrets Manager read access for the API key stored in Step 1.
cd lambda-worker
make cfn-execution-rolePrints the role ARN when complete. Copy it — you need it in Step 3.
On first run this creates the Lambda function; on subsequent runs it updates the code only.
make deploy \
EXECUTION_ROLE=arn:aws:iam::<your-aws-account-id>:role/serverless-webinar-worker-execution-roleThis cross-compiles the Go binary for linux/amd64, packages it, and creates
(or updates) the Lambda function with a 600-second timeout and 256 MB memory.
Note the Lambda ARN printed in the output — you need it in Step 5.
Set the Temporal connection config and credentials on the deployed function. Use Option A or Option B for the API key depending on what you chose in Step 1.
aws lambda update-function-configuration \
--function-name serverless-webinar-worker \
--environment "Variables={
TEMPORAL_ADDRESS=<your-namespace>.<account-id>.tmprl.cloud:7233,
TEMPORAL_NAMESPACE=<your-namespace>.<account-id>,
TEMPORAL_API_KEY_SECRET_ARN=arn:aws:secretsmanager:<your-region>:<your-aws-account-id>:secret:temporal/serverless-webinar/api-key-<suffix>,
WORKER_MAX_CONCURRENT_ACTIVITIES=5,
WORKER_MAX_CONCURRENT_WORKFLOWS=5
}" \
--region <your-region> \
--profile <your-profile>See the Configuration reference section for concurrency tuning guidance.
This step is done entirely in the Temporal Cloud UI. It wires the Lambda ARN to Temporal and creates the IAM role Temporal uses to invoke it.
- Open your namespace in the Temporal Cloud UI.
- Navigate to Workers → Serverless and click Create Worker Deployment.
- Fill in:
- Name:
serverless-webinar - Build ID:
1.0.0 - Compute: AWS Lambda
- Lambda ARN: the ARN from Step 3
- Name:
- The right panel shows a CloudFormation template. Deploy it to create the IAM
role that Temporal Cloud uses to invoke your Lambda:
- Copy or download the template from the UI
- Deploy it:
aws cloudformation deploy \ --stack-name temporal-webinar-invoke-role \ --template-file <downloaded-template.yaml> \ --capabilities CAPABILITY_NAMED_IAM \ --region <your-region> \ --profile <your-profile>
- Note the IAM Role ARN from the stack outputs
- Back in the Temporal Cloud UI, fill in:
- IAM Role ARN: the ARN from the CloudFormation stack above
- External ID: pre-filled by the UI (matches the deployment name)
- Optionally expand Show Scaling and Limits to configure min/max instances before submitting.
- Click Create.
After creation, Temporal Cloud will invoke your Lambda automatically whenever
workflows are started on the serverless-webinar task queue.
Start a test workflow to confirm end-to-end connectivity:
temporal workflow start \
--type DemoWorkflow \
--task-queue serverless-webinar \
--input '{"name":"test"}' \
--address <your-namespace>.<account-id>.tmprl.cloud:7233 \
--namespace <your-namespace>.<account-id> \
--api-key <your-temporal-api-key>Check the Temporal Cloud UI for the running workflow and the AWS Lambda console for an invocation. If it executes and completes, everything is wired up correctly.
| Step | What it does |
|---|---|
| 1 | Store Temporal API key in Secrets Manager |
| 2 | make cfn-execution-role — Lambda execution role |
| 3 | make deploy EXECUTION_ROLE=... — create Lambda + upload code |
| 4 | aws lambda update-function-configuration — set env vars |
| 5 | Temporal Cloud UI — deploy CFN template, create worker deployment |
| 6 | temporal workflow start — end-to-end smoke test |
Once the Lambda function and IAM roles exist, redeploy is just:
cd lambda-worker
make deploy EXECUTION_ROLE=arn:aws:iam::<your-aws-account-id>:role/serverless-webinar-worker-execution-roleIf you bump the BuildID in lambda-worker/main.go, open the Temporal Cloud UI
and create a new deployment version with the updated build ID, then set it as
current.
- Docker and
kubectlconfigured for the SA EKS cluster - An ECR repository for the demo app image
# Authenticate Docker to ECR
aws ecr get-login-password \
--region us-east-1 \
--profile <your-profile> \
| docker login --username AWS --password-stdin \
<your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com
# Build from the repo root (Dockerfile references ../shared)
docker build \
-t <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/serverless-webinar-app:latest \
-f demo-app/Dockerfile .
docker push <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/serverless-webinar-app:latestThe demo app reads TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, and
TEMPORAL_API_KEY from environment variables at startup (via
workerconfig.BuildClientOptions()). Store them in a k8s Secret:
kubectl create secret generic temporal-serverless-webinar \
--from-literal=serverless-webinar-temporal-address='<your-namespace>.<account-id>.tmprl.cloud:7233' \
--from-literal=serverless-webinar-temporal-namespace='<your-namespace>.<account-id>' \
--from-literal=serverless-webinar-temporal-api-key='<your-temporal-api-key>'One placeholder needs filling in before you apply:
demo-app/k8s/deployment.yaml — replace the ECR image URI:
image: <your-aws-account-id>.dkr.ecr.<your-region>.amazonaws.com/serverless-webinar-app:latestAll other values (secret key names, container port) are already correct.
Note: This is set up for an EKS cluster using Traefik + a cert for https, and as such includes
ingressroute.yamlandcertficiate.yaml. The ALB forwards all traffic to Traefik which routes based on theHostheader to your service.
kubectl apply -f demo-app/k8s/certificate.yaml -n serverless-webinar
kubectl apply -f demo-app/k8s/service.yaml -n serverless-webinar
kubectl apply -f demo-app/k8s/deployment.yaml -n serverless-webinar
kubectl apply -f demo-app/k8s/ingressroute.yaml -n serverless-webinarWait for the TLS certificate to be issued by cert-manager before testing the URL — this typically takes 30–60 seconds:
kubectl get certificate -n serverless-webinar -wWait for READY to show True, then check rollout status:
kubectl rollout status deployment/serverless-webinar-app -n serverless-webinarThe demo app will be available at the hostname configured in ingressroute.yaml
(currently https://serverless-webinar.tmprl-demo.cloud).
The UI has a hidden presenter panel for triggering workflow bursts during the live demo. Not visible to the audience by default.
Activate by adding ?presenter=1 to the URL before screen sharing
The panel fires N workflows simultaneously (default 30, max 200) to prime the scaling visuals before or during the audience participation moment.
| Variable | Required | Description |
|---|---|---|
TEMPORAL_ADDRESS |
Yes | Temporal Cloud gRPC endpoint (<namespace>.<account>.tmprl.cloud:7233) |
TEMPORAL_NAMESPACE |
Yes | Temporal namespace ID (<namespace>.<account>) |
TEMPORAL_API_KEY |
Either/or | API key value directly (Option A) |
TEMPORAL_API_KEY_SECRET_ARN |
Either/or | Secrets Manager ARN to fetch the key from at cold start (Option B) |
WORKER_MAX_CONCURRENT_ACTIVITIES |
No | Max activity tasks per invocation (default: 5) |
WORKER_MAX_CONCURRENT_WORKFLOWS |
No | Max workflow tasks per invocation (default: 5) |
The .env file in lambda-worker/ documents all variables but is not bundled
in the deployment zip.
| Variable | Required | Description |
|---|---|---|
TEMPORAL_ADDRESS |
No | Temporal server address (default: localhost:7233) |
TEMPORAL_NAMESPACE |
No | Temporal namespace (default: default) |
TEMPORAL_API_KEY |
No | Temporal Cloud API key |
Both the local worker and the Lambda worker read WORKER_MAX_CONCURRENT_ACTIVITIES
and WORKER_MAX_CONCURRENT_WORKFLOWS via shared/workerconfig. The startup log
always prints the values in use.
With WORKER_MAX_CONCURRENT_ACTIVITIES=5 and each SimulateWork activity
sleeping for 12 seconds, the worker saturates at 5 concurrent workflows. A seed
burst of 30 immediately queues 25 activity tasks, driving the sync match rate
down and triggering Lambda scaling.
To update the values on the deployed Lambda:
aws lambda update-function-configuration \
--function-name serverless-webinar-worker \
--environment "Variables={WORKER_MAX_CONCURRENT_ACTIVITIES=5,WORKER_MAX_CONCURRENT_WORKFLOWS=5}" \
--region <your-region> \
--profile <your-profile>Control how long each workflow holds worker capacity. Edit the constants in
shared/activities/demo_activities.go:
const (
WorkDuration1 = 12 * time.Second
WorkDuration2 = 12 * time.Second
WorkDuration3 = 12 * time.Second
)Each workflow chains all three sequentially (~36 seconds total worker
occupancy). StartToCloseTimeout in shared/workflows/demo_workflow.go must
always exceed the sum of all three.
Lambda timeout is set to 600 seconds at creation — well above the 36-second activity chain. Lambda memory is set to 256 MB; increase if you observe memory pressure in CloudWatch Logs.