blog.rust.gay

AWS ships a local emulator for testing cloud code without the cloud

Testing code that talks to AWS usually means choosing between two bad options. You can point tests at a real account, which is slow, costs money, and makes CI depend on credentials and network access. Or you can mock the SDK, which verifies that your code calls the functions you expect but not that the calls behave the way AWS does. AWS Emulator is an attempt at a third option: a local program that serves an AWS API endpoint, so your existing SDK and CLI talk to it as if it were the cloud.

The project is open source, licensed CC0-1.0, and has picked up a few thousand stars since it appeared. It is explicitly a tool for building and testing, not for production workloads or for security testing. That framing is honest and important. An emulator is a test double, and the value of a test double depends on how clearly you understand where it diverges from the real thing.

What the emulator is and is not

The README describes AWS Emulator as a local emulator of the AWS APIs. It has no graphical interface. Once it is running, you set environment variables so the AWS CLI or any AWS SDK sends requests to the emulator's endpoint instead of to AWS. Your code does not change. The emulator holds in-memory state for the resources you create, so a test can create a bucket, put an object, read it back, and assert on the result without any of that leaving the machine.

The "no AWS account" property is the headline. It means a new contributor can clone a repository, run the test suite, and get meaningful results without being onboarded to a cloud account. It also means CI can run on a fork's pull request without exposing credentials, which is a security win independent of cost.

What the emulator is not is a simulation of AWS's operational behavior. It does not model eventual consistency across regions, service quotas, IAM evaluation edge cases, or the failure modes you see when a dependency is throttling. Those are exactly the things that cause production incidents, and no local emulator can reproduce them faithfully. The documentation is direct about this: do not use it for production workloads or to test security. Treat it as a way to catch integration mistakes early, not as a substitute for a staging environment.

Server mode and embedded mode

There are two ways to run it. In server mode, you install a package and start a process. On Linux, AWS publishes RPM and Debian packages; on Windows and macOS, the recommended path is a container built from a short Dockerfile. The emulator then serves HTTPS on 127.0.0.1:9297 with a self-signed certificate, and you point your SDK at that endpoint with environment variables. This is the mode that works with any language and any SDK, because from the SDK's point of view it is just another HTTP endpoint.

In embedded mode, the emulator is a Rust library that runs in-process. You add it as a dev-dependency, create one isolated instance per test, and wire the emulator's HTTP client directly into the SDK client. There is no endpoint URL, no certificate, and no environment variable to manage. Each test gets its own state, so tests can run in parallel without interfering with each other.

The embedded mode is the more interesting design. Most local cloud emulators are external processes, which means tests have to start and stop a server, wait for it to become healthy, and clean up shared state between cases. That is a lot of incidental complexity, and it is a common source of flaky tests. Running the emulator inside the test binary removes the lifecycle problem entirely. The trade-off is build cost: embedded mode compiles the emulator's service crates into your test binary, so the first cargo test is much slower than usual.

The Rust test story

The repository is a Cargo workspace with one crate per AWS service, plus core crates for the router and the embedded facade. Each service crate contains a Smithy model that drives the wire protocol, a build script that generates the service declaration, and modules for request routing, in-memory state, and individual operations. That structure is worth studying even if you never use the emulator, because it is a clean example of how to turn an API description into a testable server.

For a Rust project, embedded mode changes the shape of integration tests. Instead of a test that spins up a container and hopes the port is free, you write a test that constructs an emulator, constructs a client pointed at it, and exercises your code. The test is deterministic, fast after the first build, and does not require Docker in CI. The documentation recommends reusing one CARGO_TARGET_DIR across projects, caching target/ or using sccache, and pinning the emulator to a release commit so it is not rebuilt on every dependency change.

The build requirements are real: a Rust toolchain, a C compiler, a multi-core machine, and at least 100 GB of free disk for target/. That last number is not a typo. A debug build of every service crate is large, and it grows as services are added. Embedded mode is therefore a better fit for a team that already builds a large Rust workspace than for a small project that just wants to test one S3 call.

Fidelity, not perfection

The emulator publishes fidelity scores with each release, and tracks gaps in the repository's issues/ directory. APIs and behaviors that do not work yet live under issues/things-that-do-not-work/, and fixed ones move to issues/things-that-now-work/. This is a more honest approach than claiming full compatibility, and it gives users a way to check whether their specific workload is covered before they invest in the tool.

Fidelity is the right axis to evaluate any emulator. A mock that returns a canned response tests your code's control flow. An emulator that maintains state tests whether your sequence of operations is valid. Neither tests whether AWS would accept the request under its real quotas, IAM policy, or eventual-consistency rules. The practical rule is to use the emulator for the fast inner loop and a real account for the slow outer loop, and to be explicit about which behaviors each layer covers.

The CC0 license is also notable. AWS has released the emulator into the public domain, with the vendor/ directory under Apache-2.0. That makes it easy to embed in commercial products and internal tools, which is presumably the point. It also means the project does not take pull requests; bugs and fidelity gaps are reported as issues, and the code is maintained by AWS.

Telemetry and trust

A local emulator that collects telemetry by default is a design choice worth scrutinizing. The README says the emulator reports usage data to AWS, that telemetry is on by default, and that the data is minimized so it cannot identify users, accounts, or organizations. It says records never include request or response contents, code, credentials, or personal data.

The stated intent is to understand how the tool is used. The concern is that a tool whose selling point is "your requests never leave the machine" now sends something to AWS unless you opt out. The project provides four ways to turn it off: the --telemetry off flag, the EMULATOR_TELEMETRY=off environment variable, a config.toml setting, and an embedded configuration API. The README also tells people who embed the emulator in their own tools to disclose telemetry to their users and give them a way to disable it.

That is a reasonable disclosure, but it is still a default that favors the vendor. Teams with strict data-handling rules should set EMULATOR_TELEMETRY=off in their base images and CI configuration, and should verify that the setting propagates to embedded instances. The telemetry does not change the technical value of the emulator, but it is the kind of detail that should be decided deliberately rather than inherited.

How it compares to LocalStack and Moto

LocalStack is the best-known local AWS emulator. It covers a broad set of services and has a mature ecosystem, but it is a separate product with its own licensing tiers, and its fidelity varies by service. Moto is a Python library that patches botocore, which makes it convenient for Python tests but awkward for other languages and for testing the wire protocol itself.

AWS Emulator's differentiators are its official provenance, its Rust implementation, and its embedded mode. Official provenance does not guarantee fidelity, but it does mean the Smithy models and the API behavior are more likely to match AWS's own definitions. Embedded mode is a genuine architectural advantage for Rust projects, and the one-crate-per-service layout makes it easy to see what is supported.

The weakness is maturity. LocalStack has years of production use and a large community; AWS Emulator is new, has a smaller service surface, and explicitly does not take contributions. Teams that need broad coverage today should probably keep LocalStack in the loop. Teams that write Rust and want fast, in-process tests for a handful of core services have a compelling new option.

Where it fits in a real test strategy

AWS Emulator should be viewed as a middle layer in a pyramid. The pyramid starts with unit tests that don't hit AWS at all. The next level up is emulator tests. These test the correctness of your code in sequencing API calls and processing the responses. The next level up is integration tests against an actual account. This catches all the things the emulator doesn't cover. At the top of the pyramid is a staging environment that replicates production.

The emulator is middle layer because it's quicker and cheaper than the layers on top, but more realistic than the layer below it. A test that creates a queue, sends a message and reads it back will test real serialization, real request signing, real error shapes. It's much more valuable than a test that checks that a mocked client was called with certain arguments. And it'll take milliseconds, not seconds.

So avoid the trap of using the emulator as a substitute for the layers above it. Just because you have a green emulator test suite doesn't mean your code will work in production. It means that the way your code interacts with the API is structurally correct. The teams who understand this get the most value out of it. Teams who don't eventually release a bug that only shows up against the real service.

There's also the collaboration aspect. By including the emulator in your test setup (in the repo), a new contributor can run the whole test suite without needing to ask for credentials. This speeds up onboarding and makes it easier to accept external contributions. The downside is the build time. It's significant for embedded mode, but it's a one-off cost you can cache in your CI.

Conclusion

AWS Emulator is a nice addition to the local-first testing toolkit. Particularly for Rust projects that are trying to test cloud code without relying on the network or needing an account. The most interesting thing is the embedded mode. This embeds the emulator into a library, gets rid of the server lifecycle issue, and speeds up tests and makes them less flaky. But there are caveats. Partial fidelity. Heavy build. Default telemetry. It's new.

The way you should adopt it is incrementally. Start with the services that your tests are actually using. Read the fidelity notes to see what you're depending on. Add a test against a real account for the things the emulator doesn't cover. If you use it this way, then it will speed up the inner loop without giving you false confidence in production. That's what a good test double should do. And it's a nice addition to the test stack for a Rust project.

Sources

  • AWS Emulator repository and README: https://github.com/aws/aws-emulator
  • AWS Emulator User Guide: https://docs.aws.amazon.com/emulator/latest/userguide/what-is-service.html
  • Security in AWS Emulator: https://docs.aws.amazon.com/emulator/latest/userguide/security.html
  • Telemetry in AWS Emulator: https://docs.aws.amazon.com/emulator/latest/userguide/telemetry.html
  • Getting started with embedded mode: https://docs.aws.amazon.com/emulator/latest/userguide/getting-started-embedded.html

← back to posts