LocalStack: AWS Development Without the Cloud Bill
Waiting on a real AWS environment to run integration tests is a tax on developer velocity. A five-second S3 operation becomes a minute of credential setup, cross-account IAM negotiation, and cleanup scripts that sometimes don’t run. LocalStack replaces the AWS endpoint with a single Docker container, turning us-east-1 into localhost:4566. The pitch is simple; the reality is more nuanced — and in early 2026, the project underwent structural changes significant enough that documentation from a year ago is actively misleading. This post covers how LocalStack works today, what changed, and how to use it without building false confidence in code that will fail in production.
The 2026 Structural Changes You Need to Know First
Before touching any configuration, understand what changed in March 2026:
The community edition was archived. The localstack/localstack open-source repository was archived on March 23, 2026 (the 2026.03.0 release). Both the localstack/localstack and localstack/localstack-pro Docker Hub images now point to the same unified image. What you can access is determined entirely by the entitlements on your auth token.
Auth is now required. Until April 6, 2026, no-auth startup was temporarily possible. That window is closed. A free Hobby account at app.localstack.cloud is required to run anything. Hobby is non-commercial only; commercial use starts at the Base tier.
Calendar versioning replaced semantic versioning. The last semantic release was 4.x. The current release is 2026.05.0. Changelog entries from “LocalStack 3.x” or “LocalStack 4.x” documentation refer to a previous era.
Service tiers:
| Tier | Price | Services | Use case |
|---|---|---|---|
| Hobby | Free | 30+ | Personal projects, experimentation, non-commercial CI |
| Base | $39/mo | 55+ | Commercial dev, adds IAM enforcement + persistence |
| Ultimate | $89/mo | 110+ | Full service coverage, Cloud Pods, AWS Replicator |
| Enterprise | Custom | All | Air-gapped, Kubernetes operator, on-prem delivery |
The practical impact: if you were using the old community image for free in a commercial product, that is no longer permitted under the terms of service. The old image tags remain on Docker Hub but receive no security patches or feature updates.
Architecture: One Container, One Port
LocalStack runs as a Docker container that presents a single gateway at localhost:4566. This single endpoint routes all AWS service calls — the same path-based and header-based routing used by real AWS. S3, SQS, Lambda, DynamoDB, SNS, IAM, STS, and every other emulated service respond on port 4566.
Some services require running real external processes (OpenSearch, RDS, ElastiCache). These are spawned as child Docker containers in the port range 4510–4559. LocalStack returns the correct endpoint URL from the describe API — you just need to ensure those ports are also mapped.
Your application
│
│ http://localhost:4566
▼
┌─────────────────────────┐
│ LocalStack Gateway │ :4566
│ ┌──────┐ ┌──────────┐ │
│ │ S3 │ │ DynamoDB │ │
│ └──────┘ └──────────┘ │
│ ┌──────┐ ┌──────────┐ │
│ │ SQS │ │ Lambda │──┼──► Docker socket
│ └──────┘ └──────────┘ │ │
│ ┌──────┐ │ ▼
│ │ IAM │ │ Lambda containers
│ └──────┘ │ (per-function, isolated)
└─────────────────────────┘
│ external services
▼
:4510-4559
OpenSearch, RDS instances
(separate Docker containers)
The dummy AWS account ID is always 000000000000. ARNs you create locally will look like arn:aws:s3:::my-bucket and arn:aws:lambda:us-east-1:000000000000:function:my-fn.
Installation
The lstk CLI (recommended for new setups)
LocalStack now ships a Go binary called lstk alongside the older Python-based localstack CLI:
|
|
|
|
One gotcha: if you previously ran lstk login, the keyring token takes priority over LOCALSTACK_AUTH_TOKEN in your environment. Run lstk logout first to clear it before switching auth methods.
The localstack CLI (Python, still maintained)
|
|
awscli-local installs the awslocal wrapper, which automatically appends --endpoint-url http://localhost:4566 to every AWS CLI call. Use awslocal everywhere you’d use aws.
Docker Compose Setup
|
|
The Docker socket mount is required if you use Lambda — LocalStack spawns separate containers for each function execution using the official AWS Lambda base images.
Key Environment Variables
| Variable | Default | Notes |
|---|---|---|
LOCALSTACK_AUTH_TOKEN |
— | Required. Get from app.localstack.cloud |
DEBUG |
0 |
Set to 1 for verbose logging |
PERSISTENCE |
0 |
Retain state across restarts (Base+) |
ENFORCE_IAM |
0 |
Enable IAM policy enforcement (Base+) |
IAM_SOFT_MODE |
0 |
Log IAM denials without blocking; use before hard enforcement |
SERVICES |
all | Comma-separated restrict list: s3,sqs,lambda |
SNAPSHOT_SAVE_STRATEGY |
SCHEDULED |
SCHEDULED/ON_REQUEST/ON_SHUTDOWN/MANUAL |
LAMBDA_KEEPALIVE_MS |
600000 |
Set to 0 to force cold starts every invocation |
DYNAMODB_IN_MEMORY |
0 |
Faster DynamoDB without persistence |
OPENSEARCH_ENDPOINT_STRATEGY |
domain |
Use port to expose OpenSearch on 4510-4559 range |
Do not use DATA_DIR, DEFAULT_REGION, LAMBDA_EXECUTOR, or <SERVICE>_BACKEND — these were removed in recent versions.
Check health and init hook status:
|
|
AWS CLI and SDK Configuration
|
|
AWS Profile Configuration
|
|
With endpoint_url in the profile, the AWS CLI v2 routes all calls through LocalStack without --endpoint-url flags. The credentials are dummy values — LocalStack does not validate them unless ENFORCE_IAM=1 is set.
Terraform Integration
Two approaches: manual provider configuration and the tflocal wrapper.
Manual Provider Block
|
|
Note the S3 endpoint: http://s3.localhost.localstack.cloud:4566. The domain localhost.localstack.cloud resolves to 127.0.0.1 in DNS — this is required for virtual-hosted-style bucket access. Combined with s3_use_path_style = true, it ensures S3 requests route correctly regardless of bucket name format.
tflocal Wrapper (Recommended)
|
|
tflocal generates a temporary localstack_providers_override.tf pointing all service endpoints to LocalStack. Your existing .tf files need zero modifications. The override file is created per run and ignored by git if you add it to .gitignore.
Practical Example: S3 + DynamoDB + Lambda
|
|
CDK Integration
|
|
|
|
cdklocal is a thin wrapper that sets AWS_ENDPOINT_URL=http://localhost:4566 and dummy credentials before delegating to cdk. No CDK application code changes are required.
Practical gotchas:
- CloudFormation stack updates in LocalStack are unreliable. The recommended workflow is
cdklocal destroy && cdklocal deployrather than relying on stack updates. This also matches what happens when an update fails in AWS — you learn about the rollback behavior locally. - ARNs in stack outputs will contain
000000000000. If your CDK code constructs cross-stack references that assume real account IDs, they will break. - The CDK bootstrap stack (
CDKToolkit) is created in LocalStack but its resources are minimal — no real ECR or S3 with versioning, just enough to let the deploy proceed. - CloudFormation coverage is substantial but not 1:1. Custom Resources and
cfn-signalwaiters may not behave identically.
Lambda in LocalStack
Lambda is one of LocalStack’s strongest emulations. Functions run in separate Docker containers using the official AWS Lambda base images from public.ecr.aws/lambda/.
Supported runtimes (current as of 2026.05.0): python3.13, python3.12, python3.11, nodejs22.x, nodejs20.x, java21, java17, dotnet9, dotnet8, go1.x, ruby4.0, provided.al2023. Query the live list:
|
|
Execution environment differences vs real AWS:
| Behavior | AWS | LocalStack |
|---|---|---|
/tmp persistence |
Cleared between warm invocations | Persists on same warm container |
| Cold start timing | 100ms–10s | Typically 1–3s (Docker image pull on first invoke) |
| Concurrency | Full provisioned/reserved | Limited; parallel invocations have less isolation |
| Response streaming | Supported | Not supported |
| VPC networking | Real ENI injection | DNS routing only; no actual network isolation |
| Function create | Synchronous (becomes Active) | Async — brief Pending state before Active |
The /tmp persistence difference is the most common source of bugs. If your function relies on /tmp being cleared between invocations (a correctness assumption, not a performance one), set LAMBDA_KEEPALIVE_MS=0 to force cold starts.
Hot Reloading
Hot reloading watches your source directory and reloads the Lambda without a redeploy cycle. Detection latency is approximately 700ms.
|
|
Edit your source file. The next invocation picks up the change. Hot reloading requires the hot-reload magic bucket name and an absolute host path in S3Key. It does not work with zip deployments.
State Persistence
All state is ephemeral by default. Container restart = blank slate. This is usually what you want in CI; it is not what you want in local development when you spent 10 minutes seeding a DynamoDB table.
Persistence Mode (Base+)
|
|
| Snapshot Strategy | Behavior | When to use |
|---|---|---|
SCHEDULED |
Every 15 seconds for modified services | Default; best balance |
ON_REQUEST |
After every mutating API call | Maximum durability, higher overhead |
ON_SHUTDOWN |
On container stop | No overhead; loses data on crash |
MANUAL |
Never automatically | Full control via API |
Manual save/load:
|
|
Cloud Pods (Base+)
Cloud Pods are versioned, shareable state snapshots stored in LocalStack’s cloud. They solve the “I need everyone on the team to start from the same seed state” problem:
|
|
Storage limits: Base = 300 MB/workspace, Ultimate = 3 GB/workspace, Enterprise = 5 GB/user. Snapshots are not compatible across major version boundaries — the 2026.03.0 release broke compatibility with pods created on older versions.
Initialization Hooks
Scripts mounted into the init directories execute automatically at specific lifecycle stages:
/etc/localstack/init/
├── boot.d/ # before LocalStack runtime starts
├── start.d/ # during startup
├── ready.d/ # when LocalStack is ready to serve requests ← use this
└── shutdown.d/ # on shutdown
Scripts in ready.d/ run in alphanumerical order. Use numeric prefixes for ordering:
|
|
|
|
|
|
In Docker Compose:
|
|
Scripts must be executable (chmod +x). Check completion status:
|
|
CI/CD with GitHub Actions
|
|
setup-localstack@v0.3.2 (April 17, 2026) is the current stable version. The LOCALSTACK_AUTH_TOKEN secret needs to be added to your repository settings.
Setting AWS_ENDPOINT_URL in the test environment is the cleanest integration — recent versions of boto3 and the AWS SDK for JavaScript pick it up automatically without per-client configuration.
pytest Integration
|
|
If AWS_ENDPOINT_URL is set in the environment, boto3 ≥ 1.28 picks it up automatically. You can simplify fixtures to just boto3.client("s3") in that case — useful for running the same test suite against both LocalStack and real AWS by toggling the env var.
Service-to-Service Testing with Docker Networks
When your application container needs to call LocalStack, localhost from inside the container is the container itself, not the host. Use the container name instead:
|
|
The condition: service_healthy requires the healthcheck block on the LocalStack service so your app container doesn’t start before LocalStack is ready.
The Fidelity Gaps That Matter
This section is the most important part of this post. LocalStack is not AWS. Code that passes locally can fail in production, and the failure modes follow predictable patterns.
IAM Is Off by Default — This Is Dangerous
By default (ENFORCE_IAM=0), LocalStack accepts every API call regardless of IAM policy. Your Lambda can read any S3 bucket. Your ECS task can describe every EC2 instance. Code that should fail with AccessDeniedException will succeed silently. This is the primary source of “works locally, fails in prod” stories.
Fix: enable ENFORCE_IAM=1 (requires Base tier). Do this with IAM_SOFT_MODE=1 first — it logs denials without blocking, letting you discover what permissions your application actually needs before switching to hard enforcement.
|
|
Even with enforcement on, some gaps exist:
- Policy variables in Resource elements (e.g.,
${aws:username}) are not evaluated - Organizations principal, Federated, and CanonicalUser principal types are unsupported
- Some cross-service enforcement is missing (API Gateway to S3, Lambda to Kafka)
VPC Is Cosmetic
VPCs, subnets, security groups, and route tables can be created and described via API, but the network topology is not simulated. Security group rules have no effect. Lambda functions placed in a VPC still reach the internet (and still reach LocalStack). NACLs are silently accepted and ignored.
This means any connectivity issue that depends on security group rules, subnet routing, or NACL deny rules will not be caught locally.
Throttling and Service Quotas Don’t Exist
LocalStack never returns ThrottlingException, ServiceQuotaExceededException, or TooManyRequestsException. Code that handles AWS rate limits can’t be tested for correctness against LocalStack — you need a real AWS account with throttled credentials, or a mock layer that injects these errors.
Other Service-Specific Gaps
Cognito: JWT signature, token issuer, and expiration are not validated. An expired Cognito token will succeed locally. The USER_AUTH flow works for basic cases but has edge-case gaps.
Step Functions: Standard workflow execution works. Express Workflows and real service integration error handling diverge from AWS in edge cases. Use LocalStack’s MockedServiceIntegrations feature for deterministic testing of state machines.
API Gateway: REST API (v1) and HTTP API (v2) coverage is good. WebSocket APIs have limited support. Custom domain names don’t function for Hobby/Base tiers. JWT authorizers work against Cognito but not external OIDC.
CloudFormation: Stack creates are reliable. Stack updates are not — use destroy + redeploy. Drift detection is not implemented.
RDS: The API surface is emulated; actual SQL connections go to a real PostgreSQL or MySQL process spawned locally. Aurora-specific features (failover timing, Serverless v2 pause/resume, certain replication flags) behave differently.
S3 presigned URLs: Expiry validation and signature checking differ from AWS. Don’t rely on LocalStack to catch presigned URL permission or expiry bugs.
Eventual consistency: DynamoDB Global Tables, S3 cross-region replication, and similar eventually-consistent constructs behave synchronously in LocalStack. Code that handles replication lag won’t be exercised.
Alternatives After the Community Edition Archival
The March 2026 archival prompted several open-source alternatives aimed at the gap:
| Tool | Approach | Strengths | Limitations |
|---|---|---|---|
| Moto | Python library, mocks AWS SDK calls via decorators | Zero dependencies, fast, excellent for unit tests | Python-only, no real HTTP endpoint |
| AWS SAM Local | Lambda + API Gateway emulator only | Tight Lambda fidelity, free | Single service pair, no S3/DynamoDB/etc. |
| Testcontainers | Real service Docker images per test (DynamoDB Local, ElasticMQ) | High per-service fidelity | No unified endpoint, wiring overhead |
| Floci | OSS, Quarkus-friendly, MIT licensed | Free, growing coverage | New project, limited ecosystem maturity |
| MiniStack | OSS, claims 60+ services | MIT licensed, Testcontainers modules | New project, unproven at scale |
| LocalStack | Unified Docker container, 110+ services | Most complete, mature tooling | Requires paid auth for commercial use |
For pure unit testing of business logic that happens to call AWS SDKs, Moto remains the best free option. For multi-service integration testing where you need S3, SQS, Lambda, and DynamoDB talking to each other, LocalStack’s unified endpoint model has no mature free equivalent post-archival.
Practical Patterns
Tagging for Cleanup
Tag all test resources so a cleanup sweep can find them:
|
|
Multiple Parallel LocalStack Instances
For parallel CI jobs that can’t share state, vary the gateway port and external service range:
|
|
Getting the OpenSearch Endpoint
With OPENSEARCH_ENDPOINT_STRATEGY=port, the endpoint is on a dynamic port in the external service range. Retrieve it:
|
|
The returned URL is what your application should use to connect to OpenSearch.
What’s Actually Worth Paying For
IAM Enforcement (Base, $39/mo): The single most important paid feature. Without it, you are testing against a trust-everything system that will give you false confidence. The $39/mo cost is almost certainly less than one production incident caused by a missing IAM policy.
Persistence (Base): Saves hours of re-seeding across restarts. Worth it for any team that iterates heavily on a complex local environment.
Cloud Pods (Base): Share seed state across the team and CI. Eliminates the “works on my machine because I seeded it differently” problem.
AWS Replicator (Ultimate, $89/mo): Replicates real AWS resources (S3 buckets, DynamoDB tables) into your local environment. Useful for developing against production data shapes without a staging environment.
App Inspector (Ultimate): Runtime observability that surfaces misconfigurations and missing IAM permissions in your running application. Essentially a policy advisor that watches your traffic.
The old community edition served as a “good enough” layer that let teams avoid AWS costs entirely. The new Hobby tier serves the same purpose for non-commercial use. For any commercial product, Base is the practical entry point.
Component and Version Reference
| Component | Version | Install |
|---|---|---|
| LocalStack for AWS | 2026.05.0 (calendar versioning) | Docker image |
lstk CLI |
v0.9.0 (May 21, 2026) | brew install localstack/tap/lstk |
localstack CLI |
~4.x (still maintained) | pip install localstack |
awslocal |
current | pip install awscli-local |
cdklocal |
current | npm install -g aws-cdk-local |
tflocal |
current | pip install terraform-local |
setup-localstack GitHub Action |
v0.3.2 (April 17, 2026) | LocalStack/setup-localstack@v0.3.2 |
| LocalStack Desktop | 2.0+ | Download from app.localstack.cloud |
LocalStack is the most complete AWS emulator available, and the multi-service integration testing use case — where S3, SQS, Lambda, and DynamoDB interact with each other through real HTTP — has no mature free alternative since the community archival. The constraint is the IAM gap: always develop with ENFORCE_IAM=1 on Base or higher, or you are testing a version of your application that will never exist in production.
Comments