AWS Emulator went generally available on 11 November 2026. It is a single binary from AWS that answers AWS API calls on your own machine: no account, no charge, and nothing leaving the laptop unless you leave telemetry switched on. Local stand-ins for AWS are not new; moto and LocalStack have carried that load for a decade. What is new is the publisher, and the way it was built. AWS says the code was written by autonomous agents from the published API models, and that nobody on the team wrote or reviewed it.
A claim like that is settled by running the thing, so this is a lab notebook and not a review. I gave the emulator a clean CI runner, a list of ten questions, and a script for each one. Every number and every terminal capture below comes from one run of that script against release 1.0.341, the build that was current on 13 November. Where the documentation says one thing and the run showed another, both are written down.
Each section below has the same three parts: the question, what was run, and what came back. If you have five minutes, read the summary and the assessment table. If you are deciding whether to put this in a pipeline, read the sections in full.
Summary
- It starts fast and it is not small. A signed 176.5 MB Debian package unpacks to a 951 MiB binary. The process was healthy 0.17 seconds after launch and held 290 MiB of memory before any work.
- Existing tools worked unmodified. The AWS CLI, boto3, and Terraform with the stock AWS provider all ran against it with environment variables only.
- It behaves more like AWS than like a mock where tests usually go wrong. A new DynamoDB table sits in
CREATINGfor three seconds, and SNS and EventBridge will not deliver to an SQS queue that has no queue policy. - Reach is wide and depth is uneven. 256 of 436 services answered a read call. AWS’s own published scores show 60.2% of its AWS-verified tests passing in this release, from 78% for KMS down to 20% for CloudFormation.
- It is not a place to test permissions. A brand-new IAM user with no policy at all listed S3 buckets without an error.
- Nothing survives a restart. State, credentials and the TLS certificate are all per process and all in memory.
Test environment
One GitHub-hosted Linux runner, x86_64, 4 vCPU. The same boto3 scripts were pointed first at the emulator and then at a moto server, so that each result has something to be compared with.
| Component | Version in the run |
|---|---|
| AWS Emulator | 1.0.341 (reports release date 2026.11.13) |
| AWS CLI | 2.39.3 |
| Python and boto3 | 3.13.18 and 1.44.2 |
| moto (server mode) | 5.2.5 |
| Terraform | 1.16.5 |
| Terraform providers | hashicorp/aws 6.73.0, hashicorp/random 3.9.1 |
What AWS Emulator is and how it was built
AWS Emulator is a Rust program that serves the AWS APIs over HTTPS on 127.0.0.1:9297. Its service front ends are generated from the Smithy models AWS already publishes for its SDKs, which is why the list of services is so long on day one. The repository is public, the code is released under CC0 1.0 with vendored dependencies under Apache 2.0, and it does not accept pull requests. Defects are filed as issues and tracked in two directories whose names say what they hold: things that do not work, and things that now work.
The build process is the unusual part. According to AWS’s own account, three groups of agents work in a loop. One group writes tests and verifies each of them against the real service in a live Region. A second group hunts for differences between the emulator and AWS. A third fixes them. A change merges only when it passes a gate of four checks: it builds, it meets the coding standards, it passes the security checks, and it does not lower the fidelity score. No human reads the diff.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3E0','primaryTextColor':'#232F3E','primaryBorderColor':'#FF9900','lineColor':'#232F3E','clusterBkg':'#F8FAFC','clusterBorder':'#CBD5E1','edgeLabelBackground':'#FFFFFF','nodeTextColor':'#232F3E'}}}%%
flowchart TD
M["Smithy API models
published by AWS"] --> W["Agents write tests"]
W --> V["Each test verified
against real AWS"]
V --> F["Agents find differences"]
F --> X["Agents write fixes"]
X --> GATE{"Merge gate:
build, standards,
security, fidelity"}
GATE -- "fails" --> X
GATE -- "passes" --> R["Release with
published scores"]
R --> F
style M fill:#EBF8FF,stroke:#0073BB,stroke-width:2px,color:#004C74
style V fill:#F0FFF4,stroke:#1D8102,stroke-width:2px,color:#145202
style GATE fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
style R fill:#F0FFF4,stroke:#1D8102,stroke-width:2px,color:#145202For an architect, the consequence is a change in where trust comes from. With moto you trust maintainers and a decade of bug reports. Here the only things standing behind the code are a test corpus checked against AWS and a score published with every release. That makes the score the contract. It is covered in the section on service coverage and fidelity, and it is why this article tests behaviour and does not read source.
Installing and verifying the package
Run: download the Debian package and its detached signature, import AWS’s publishing key, compare the key’s fingerprint with the one printed in the user guide, verify the signature, and only then install.

curl -fsSLo "$work/aws-emulator.deb" "$BASE/aws-emulator_amd64.deb" && curl -fsSLo "$work/aws-emulator.deb.sig" "$BASE/aws-emulator_amd64.deb.sig" || exit 2
ls -l "$work" | awk 'NR>1 {printf "%s %s bytes\n", $9, $5}'
sha256sum "$work/aws-emulator.deb" | sed "s|$work/||"
curl -fsSLo "$work/aws-publishing.asc" https://downloads.aws.com/keys/gpg/current.asc
export GNUPGHOME="$work/gnupg"; mkdir -m 700 "$GNUPGHOME"
gpg --quiet --import "$work/aws-publishing.asc" 2>&1 | tail -n 2
actual="$(gpg --with-colons --fingerprint ssm-publishing@amazon.com | awk -F: '/^fpr/ {print $10; exit}')"
log "gpg --fingerprint ssm-publishing@amazon.com"
echo "fingerprint $actual"; [ "$actual" = "$EXPECTED_FINGERPRINT" ] && echo "matches the fingerprint in the user guide" || { echo "FINGERPRINT MISMATCH"; exit 2; }
log "gpg --verify aws-emulator.deb.sig aws-emulator.deb"
gpg --verify "$work/aws-emulator.deb.sig" "$work/aws-emulator.deb" 2>&1 | grep -E "Good signature|BAD signature|using" || exit 2
Result: the fingerprint matched and GnuPG reported a good signature. The [unknown] next to the key name is GnuPG saying that nobody in a fresh keyring has vouched for the key, which is expected; the comparison with the fingerprint in the documentation is the control that matters, and the script stops if it fails.
One thing to change before this goes near a shared pipeline. The download path ends in LTS/latest, so two jobs a day apart can install different builds, and this project ships often: the release notes for 1.0.341 list 68 fixes. Copy the package into your own artefact store, record its SHA-256, and move to a new build as a deliberate change.
Start-up, footprint and configuration
Run: start the emulator on its default port with telemetry off, poll the health endpoint, list the files it wrote, and make four AWS CLI calls.

Result: healthy in 0.17 seconds. On start the process writes four files under ~/.aws/emulator/9297/, all readable by the owner only: a credentials file, a config file, a CA certificate in PEM form and the same certificate as a PKCS#12 trust store for Java. The endpoint speaks HTTPS with that per-instance certificate, and it expects requests signed with the generated key. Every caller is account 000000000000.
Memory is the number to plan around. The process held 290 MiB when idle, 432 MiB after the 22-experiment tour, and 638 MiB after one call to each of 436 services. State is held in memory and nothing is evicted, so a long test suite that creates many objects needs headroom on the runner.
Telemetry is on unless you turn it off. The lab passes --telemetry off; the documentation also lists an environment variable and a config file for the same purpose. In a regulated estate, set it off in the image and do not leave it to each engineer.
Pointing tools at the emulator takes environment variables and nothing else. These are the two shell functions the whole lab is built on:
start() { # port
local began; began="$(date +%s.%N)"
aws-emulator -p "$1" --telemetry off > "$out/emulator-$1.log" 2>&1 &
echo $! > "$work/pid-$1"
for i in $(seq 1 200); do
curl -fsS --cacert "$HOME/.aws/emulator/$1/ca.pem" "https://127.0.0.1:$1/__admin/health" > /dev/null 2>&1 && break; sleep 0.05
done
printf 'port %s healthy after %.2f s\n' "$1" "$(echo "$(date +%s.%N) - $began" | bc)"
}
rss() { awk '/VmRSS/ {printf "%.0f MiB resident\n", $2/1024}' "/proc/$(cat "$work/pid-$1")/status"; }
use() { export AWS_ENDPOINT_URL="https://127.0.0.1:$1" AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/emulator/$1/emulator_credentials" AWS_CONFIG_FILE="$HOME/.aws/emulator/$1/config" \
AWS_CA_BUNDLE="$HOME/.aws/emulator/$1/ca.pem" AWS_EC2_METADATA_DISABLED=true AWS_DISABLE_HOST_PREFIX_INJECTION=true AWS_REGION=us-east-1 AWS_DEFAULT_REGION=us-east-1 AWS_PAGER=""
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE; }
The unset at the end is deliberate. If real credentials are present in the environment they take precedence over the credentials file, so requests would be signed with a key the emulator did not issue. More to the point, a test stage should not be holding real credentials at all.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3E0','primaryTextColor':'#232F3E','primaryBorderColor':'#FF9900','lineColor':'#232F3E','clusterBkg':'#F8FAFC','clusterBorder':'#CBD5E1','edgeLabelBackground':'#FFFFFF','nodeTextColor':'#232F3E'}}}%%
flowchart TD
T["AWS CLI, boto3, Terraform"] --> E["Environment variables:
endpoint, credentials file,
config file, CA bundle"]
E --> H["HTTPS with SigV4 to
127.0.0.1:9297"]
H --> EM["AWS Emulator process"]
EM --> S["State in memory,
one account, one Region"]
EM --> FI["Files in ~/.aws/emulator/9297:
keys, config, CA certificate"]
FI -. "read by" .-> E
style EM fill:#FFF3E0,stroke:#FF9900,stroke-width:2px,color:#232F3E
style S fill:#EBF8FF,stroke:#0073BB,stroke-width:2px,color:#004C74
style FI fill:#F0FFF4,stroke:#1D8102,stroke-width:2px,color:#145202Compatibility with the AWS CLI, SDKs and Terraform
Run: the AWS CLI calls in Figure 2, a boto3 suite (covered in the next section), and a Terraform configuration that was written for real AWS and has never seen this emulator. The configuration is the one from Keeping Secrets Out of Terraform State: an ephemeral random password written to Secrets Manager through a write-only argument.

Result: init, apply and destroy all succeeded with an empty provider "aws" {} block. The provider picked up the endpoint, keys and CA bundle from the environment. Reading the secret back through the CLI returned a 32-character string, so the write-only argument reached the emulator and was stored.
Two details from the boto3 runs are worth knowing before you port a suite. First, set AWS_DISABLE_HOST_PREFIX_INJECTION=true, as the user guide says; without it some SDK calls try to prepend a host label to 127.0.0.1. Second, SQS returns queue URLs whose host is sqs.us-east-1.amazonaws.com, as the real service does. An SDK with an endpoint override ignores that host and the calls stay local, which is what happened here. Code that opens its own HTTP connection to the returned URL would leave the machine. Search your codebase for that before trusting a green run.
Behaviour compared with moto
Run: 22 experiments through boto3, chosen because each one is a place where a permissive mock lets a bug through. The same script then ran against moto 5.2.5 in server mode.

Result: all 22 ran to completion on the emulator, and 21 of 22 on moto. Completion is the less interesting part. What matters is where the two gave different answers to the same call.

Three of these change what a test can catch.
Asynchronous creation is modelled. CreateTable returned CREATING, an immediate write failed with ResourceNotFoundException, and the table became ACTIVE after three seconds. Code that forgets to wait for the table passes on moto and fails here, as it would in an account.
Validation happens before the call is accepted. A two-character table name was refused with a ValidationException whose message has the same shape as the real service’s. moto created the table.
Resource policies gate delivery between services. This is the one I did not expect. With no queue policy, a message published to SNS never reached the subscribed SQS queue, and an event matched by an EventBridge rule never reached its target queue. Add the policy that allows the service to call sqs:SendMessage and both arrive, in 19 ms. Forgetting that policy is a familiar reason an event-driven stack works in a mock and stays silent in an account. This is the experiment:
def drain(sqs, url: str, seconds: int = 6) -> list[str]:
deadline, bodies = time.time() + seconds, []
while time.time() < deadline and not bodies:
bodies = [m["Body"] for m in sqs.receive_message(QueueUrl=url, WaitTimeSeconds=1).get("Messages", [])]
return bodies
def allow(sqs, url: str, queue_arn: str, service: str, source_arn: str) -> None:
"""The resource policy AWS requires before a service may deliver to a queue."""
policy = {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": {"Service": service}, "Action": "sqs:SendMessage",
"Resource": queue_arn, "Condition": {"ArnEquals": {"aws:SourceArn": source_arn}}}]}
sqs.set_queue_attributes(QueueUrl=url, Attributes={"Policy": json.dumps(policy)})
def sns_to_sqs(with_policy: bool):
def experiment():
sns, sqs = client("sns"), client("sqs")
tag = "p" if with_policy else "n"
url = sqs.create_queue(QueueName=f"lab-fanout-{tag}-{suffix}")["QueueUrl"]
arn = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["QueueArn"])["Attributes"]["QueueArn"]
topic = sns.create_topic(Name=f"lab-topic-{tag}-{suffix}")["TopicArn"]
if with_policy:
allow(sqs, url, arn, "sns.amazonaws.com", topic)
sns.subscribe(TopicArn=topic, Protocol="sqs", Endpoint=arn)
sns.publish(TopicArn=topic, Message="order placed")
bodies = drain(sqs, url)
return {"delivered": bool(bodies), "sns_envelope": bool(bodies) and '"Type"' in bodies[0] and "order placed" in bodies[0]}
return experiment
The rest of the tour passed on both: S3 round trips, versioning and presigned URLs, SQS visibility timeouts, Secrets Manager versions, KMS encrypt and decrypt, an SSM SecureString, a Kinesis put and get, a CloudFormation stack that created a queue, and EC2 refusing an overlapping subnet with InvalidSubnet.Conflict. A Step Functions execution ran to SUCCEEDED with its output on the emulator and was still RUNNING after ten seconds on moto.
On speed, 1,000 sequential DynamoDB puts had a median of 0.98 ms on the emulator and 2.07 ms on moto, with gets at 0.97 ms and 2.17 ms. Both are fast enough that the test code, not the endpoint, sets the pace.
This lab compares two local endpoints. It did not run the same calls against a real account, so a result here says the emulator is stricter than moto, not that it is correct. For correctness, the evidence is the published fidelity score, covered under service coverage and fidelity.
Lambda execution
Run: create a Python function, invoke it twice, and have the handler report on its surroundings. It doubles a number, then attempts five things that touch the machine it runs on and returns what happened to each.
handler = (
"import os, socket, sys\n"
"def attempt(action):\n"
" try:\n"
" return str(action())\n"
" except Exception as error:\n"
" return 'failed: ' + type(error).__name__\n"
"def handler(event, context):\n"
" return {'doubled': event['n'] * 2, 'python': sys.version.split()[0],\n"
" 'getcwd': attempt(os.getcwd),\n"
" 'getuid': attempt(os.getuid),\n"
" 'entries_in_host_home': attempt(lambda: len(os.listdir(event['host_home']))),\n"
" 'bytes_of_credentials_file': attempt(lambda: len(open(event['credentials_file']).read())),\n"
" 'socket_to_endpoint': attempt(lambda: socket.create_connection(('127.0.0.1', event['port']), 2).close() or 'connected')}\n")
| What the handler reported | AWS Emulator | moto server |
|---|---|---|
| First and second invoke | 109 ms and 107 ms | 14.1 s and 0.59 s |
| Python version (runtime requested: python3.13) | 3.14.0.alpha | 3.13.17 |
| Working directory | /var/task | /var/task |
os.getuid() | failed: OSError | 993 |
| List the host home directory | failed: OSError | failed: FileNotFoundError |
| Read the emulator credentials file | failed: OSError | failed: FileNotFoundError |
| Open a socket to the endpoint on 127.0.0.1 | failed: OSError | failed: ConnectionRefusedError |
Result: two very different designs. moto starts a Docker container from a Lambda base image, which explains the 14-second first call, the real CPython 3.13 and the container’s own user and filesystem. The emulator answered in about 100 ms both times, which leaves no room for a container start, reported a Python version that is not the runtime that was requested, and refused every operating-system call the handler attempted.
Read this entry with care, because it is where the run and the documentation point in different directions. The user guide warns that function code is not isolated from your machine. In this run, four separate attempts to reach the machine were refused. I am reporting what happened, not offering it as a guarantee: one release, one runtime, five probes. Treat function code you run in the emulator with the same care as any other code you run locally.
The practical consequences are narrower and more certain. A handler that depends on behaviour specific to CPython 3.13, or on a compiled wheel, is not being exercised by the interpreter it will meet in production. And the refused socket raises a question this lab did not answer: whether a handler can call other emulated services from inside the function. That is first on the list of open questions at the end.
Service coverage and fidelity
Run: two measurements. A survey that asks every service boto3 knows one harmless read question, and a reading of the fidelity scores AWS publishes with each release.

for service in session.get_available_services():
try:
client = session.client(service, **kw)
except Exception as error: # noqa: BLE001
rows.append({"service": service, "result": "client could not be created", "detail": type(error).__name__})
tally["client could not be created"] += 1
continue
model = client.meta.service_model
chosen = None
for name in sorted(model.operation_names):
if not name.startswith(("List", "Describe", "Get")):
continue
shape = model.operation_model(name).input_shape
if shape is None or not shape.required_members:
chosen = name
break
if chosen is None:
rows.append({"service": service, "result": "no parameterless read operation"})
tally["no parameterless read operation"] += 1
continue
method = next(py for py, api in client.meta.method_to_api_mapping.items() if api == chosen)
started = time.perf_counter()
try:
getattr(client, method)()
result = "answers"
except botocore.exceptions.ClientError as error:
status = error.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
result = "not implemented (501)" if status == 501 else f"error {status}"
except Exception as error: # noqa: BLE001
result = f"transport: {type(error).__name__}"
rows.append({"service": service, "operation": chosen, "result": result, "ms": round((time.perf_counter() - started) * 1000, 1)})
tally[result.split(":")[0] if result.startswith("transport") else result] += 1
Result, reach: 256 services answered. 78 returned HTTP 501 with NotImplemented, which is the emulator saying plainly that the operation is modelled and not yet built. 36 returned some other error or failed at the transport level, and 66 have no read operation that can be called without parameters, so they were not probed. A 501 is the right failure. A test meets a clear error on the first call, where a permissive mock would hand back an empty success.
One call per service measures reach and says nothing about depth. For depth, AWS publishes its own numbers, and they are more candid than vendor numbers usually are.

Result, depth: the scores file for 1.0.341 covers 43,548 tests across 355 services. 26,157 pass, 17,308 fail and 83 are skipped, which is 60.2% of the scored tests. The tests come in two kinds, and the split matters:
| Kind of test | Pass | Fail | Pass rate |
|---|---|---|---|
| Behavioural: does a sequence of calls end in the right state? | 21,857 | 10,899 | 66.7% |
| Wire: is the response the same status, headers and body as AWS? | 4,300 | 6,409 | 40.2% |
| All scored tests | 26,157 | 17,308 | 60.2% |
Among the 126 services with at least 50 tests, the median pass rate is 65%. The services most teams start with sit near or above that: SQS 70%, S3 and DynamoDB 68%, SNS 76%, Step Functions and EventBridge 75%, KMS 78%. Lambda is at 57% and IAM at 60%. The weak end matters more for planning: EC2 42%, RDS 43%, SSM 28%, CloudFormation 20%.
Two readings follow. If your tests assert on outcomes, such as an item being stored or a message arriving, you are on the behavioural side where two in three tests pass. If they assert on exact error bodies, headers or status codes, you are on the wire side where most still fail; of the wire failures, 3,631 are a different status code and 2,604 a different body. And CloudFormation at 20% means that deploying a real stack through the emulator is not yet a sound plan, even though the one-queue stack in the tour worked. Terraform, which calls the service APIs directly, is the better route for now.
IAM and security
Run: three probes. Create an IAM user with no policy, sign an S3 call with that user’s own access key, and see what happens. Put a bucket policy that denies s3:GetObject and read an object. Send a request with no signature at all.
Result: the user with no permissions listed buckets without an error, and STS confirmed the call was made as that user. The emulator authenticates a request, in the sense of checking the signature against a key it knows, and does not authorise it against IAM policy. The security guide is direct about this, in a sentence every team adopting it should read: “The emulator never denies a request signed with its own key.”
The other two probes are recorded as observed, with their limits. The read after a bucket policy Deny came back AccessDenied, although the documentation says bucket and key policies are not evaluated the way AWS evaluates them; do not build a test on that behaviour. The unsigned request was not served, but the response was a 404 for an unknown service and not a signature error, so my probe did not reach the signature check and settles nothing about it. moto served the same unsigned request with a 200.
An over-broad role, a missing permission and a role with no permissions at all behave identically here. Any control that depends on IAM, a resource policy, a KMS key policy or a service control policy needs a test in a real account, or a policy analysis tool. For prescribing or other clinical workloads this is a hard line: access control evidence for an audit cannot come from an emulator.
State and multiple instances
Run: start a second emulator on port 9298, compare what each holds, stop the first with SIGTERM, start it again, and look.

Result: the two instances shared nothing. The first held the six buckets created earlier in the run; the second held none, and each had its own access key. When the first instance stopped, it removed its four files. When it started again it had no buckets and a new key.
This shapes how a suite is written. Parallel jobs get isolation for free by giving each its own port. There is no reset call to forget, because a restart is the reset and costs 0.17 seconds. The other side of that: there is no seeded state to carry between jobs, so every suite builds its fixtures from nothing, and any tool that cached the previous instance’s key must read the files again after a restart. Each instance is also one account and one Region, so cross-account and multi-Region designs are outside what a single process can represent.
Using it in a CI pipeline
The lab’s own script is the recipe, and the start-up section above lists the two functions that carry it. In order:
- Install from a pinned, hash-checked copy of the package in your own artefact store, after verifying the signature once on the way in.
- Start one emulator per job with
--telemetry off, on its own port. The help text lists-p 0for a free port; this lab used fixed ports. - Poll
/__admin/healthwith the instance’s CA certificate before the first call. Do not sleep and hope. - Export the endpoint, credentials file, config file and CA bundle, and unset any real AWS credentials in the job.
- Run the suite. Treat an HTTP 501 as “not built yet” and report it separately from a test failure.
- Mask key identifiers in logs. The keys are throwaway, but a transcript is no place for the habit of printing keys.
- Stop the process. There is nothing to clean up.
Then decide what the emulator stage is allowed to prove. My answer, after this run, is that it replaces the mock stage and does not replace the account stage.
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#FFF3E0','primaryTextColor':'#232F3E','primaryBorderColor':'#FF9900','lineColor':'#232F3E','clusterBkg':'#F8FAFC','clusterBorder':'#CBD5E1','edgeLabelBackground':'#FFFFFF','nodeTextColor':'#232F3E'}}}%%
flowchart TD
U["Unit tests
every commit"] --> EMU["Emulator stage
every push:
API calls, wiring, waits,
resource policies for delivery"]
EMU --> ACC["Real account stage
before release:
IAM, KMS and resource policies,
quotas, CloudFormation, networking"]
ACC --> REL["Release"]
style U fill:#EBF8FF,stroke:#0073BB,stroke-width:2px,color:#004C74
style EMU fill:#FFF3E0,stroke:#FF9900,stroke-width:2px,color:#232F3E
style ACC fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px,color:#4C1D95
style REL fill:#F0FFF4,stroke:#1D8102,stroke-width:2px,color:#145202Assessment summary
| Area | What the run showed | Standing |
|---|---|---|
| Install and supply chain | Signed package, fingerprint published in the docs; download URL is a moving “latest” | Ready, once pinned |
| Start-up and footprint | 0.17 s to healthy; 290 to 638 MiB resident; 951 MiB on disk | Ready |
| CLI, SDK, Terraform | All worked through environment variables alone | Ready |
| Core data and messaging services | Stricter than moto on waits, validation and delivery policies | Ready for outcome tests |
| Lambda | Fast; interpreter is not the requested runtime; host calls refused | Use with care |
| CloudFormation, EC2, RDS, SSM | 20% to 43% of published tests pass | Not yet |
| IAM and policy enforcement | A user with no policy was allowed | Not for this |
| State | In memory, per process, gone on exit | By design |
Cost
The software has no licence fee and makes no calls that AWS bills for, so the costs are the ordinary ones of a CI stage. Each job that does not cache the package downloads 176.5 MB and needs about 1 GB of disk for the binary. Memory in this run peaked at 638 MiB, comfortably inside a standard hosted runner. Start-up time is too small to matter.
Set against a shared sandbox account, three line items disappear: per-call and per-resource charges for test traffic, the cleanup of resources that a failed job leaves behind, and the long-lived cloud credentials that the test stage would otherwise need. That last one is a security gain as much as a saving. The cost that appears is less visible: you now maintain two integration stages, and somebody has to own the list of what each one is trusted to prove. Left unowned, the emulator stage slowly becomes the only stage, and the section on IAM and security describes what that misses.
Decision framework
| Your situation | Recommendation | Reason |
|---|---|---|
| Serverless or event-driven services on S3, DynamoDB, SQS, SNS, EventBridge, Step Functions | Adopt for the per-push stage | Best scores, and it catches missing waits and missing queue policies |
| Already on moto with a large green suite | Run both for a sprint and study the differences | Each new failure is either a real bug or an emulator gap, and both are worth knowing |
| Infrastructure as code through Terraform or OpenTofu | Use for fast plan and apply checks on supported services | The stock provider worked unmodified |
| Infrastructure as code through CloudFormation or CDK | Wait | 20% of CloudFormation tests pass in this release |
| Workloads centred on EC2, VPC networking or RDS | Wait, or use for narrow control-plane checks | Scores in the low 40s |
| Regulated workloads that need access-control evidence | Use for functional tests only | Policies are not enforced; evidence must come from a real account |
| Air-gapped or offline development | Adopt | No account and no network needed once the package is mirrored |
Open questions
- Can a Lambda handler call other emulated services from inside the function, given that a plain socket to the endpoint was refused?
- Do compiled Python packages, and the Node.js, Java and .NET runtimes, behave as they do in Lambda?
- How does memory grow under a large suite, and is there a ceiling?
- Does an unsigned but otherwise valid request get refused? The probe here was turned away earlier, for a different reason.
- How do the macOS and Windows builds, the embedded Rust mode and the agent skill compare with the Linux package tested here?
- How quickly do the low-scoring services move? The answer is in the scores file of each release, and it is worth tracking for the services you depend on.
Key takeaways
- AWS Emulator is a credible replacement for the mock stage of a pipeline on the mainstream serverless services, and existing CLI, SDK and Terraform tooling runs against it through environment variables alone.
- It is stricter than moto in ways that find real defects: asynchronous resource creation, request validation, and resource policies on cross-service delivery.
- The published fidelity score is the contract. At 60.2% overall, with behavioural tests at 66.7% and wire-level tests at 40.2%, check the score for each service you use before you rely on it.
- It authenticates and does not authorise. IAM, resource policy and KMS policy testing stays in a real account.
- Pin the package, turn telemetry off in the image, give each job its own port, and keep a real-account stage before release.
If you have run your own suite against it, I would like to hear which services held up and which did not. Leave a comment with the service and the release number.
Everything above ran once, in GitHub Actions on a hosted Linux runner, against AWS Emulator 1.0.341: the signature check and install, start-up timing and memory readings, 22 boto3 experiments against the emulator and against moto 5.2.5, 1,000-call DynamoDB timings, a one-call survey of 436 services, the two-instance and restart test, a Terraform apply and destroy, and a summary of the scores file AWS publishes for the release. The terminal captures are rendered from the transcripts of that run, with long lines shortened and access key identifiers masked. Timings and memory figures are single measurements on a shared runner. Not run: the same calls against a real AWS account, the macOS and Windows builds, embedded mode, the agent skill, Java and .NET clients, and Lambda runtimes other than Python. Statements about how the emulator was built come from AWS’s own publications. The run is here.
The install script, the 22 experiments, the service survey, the timing loop and the scores summariser are in aws-emulator-first-look in the samples repository. ./ci.sh out reproduces the run on Debian or Ubuntu.
Glossary
- Fidelity— How closely the emulator matches the real service. AWS measures it with tests that were first verified against AWS, and publishes the result per release.
- Behavioural test— A test that checks the outcome of a sequence of calls, such as an item being stored.
- Wire test— A test that compares the status code, headers and body of a response with what AWS returns.
- Smithy— The interface definition language AWS uses to describe its service APIs. SDKs and this emulator are generated from Smithy models.
- SigV4— Signature Version 4, the request-signing scheme for AWS APIs. The emulator checks it against keys it generates at start-up.
- 501 NotImplemented— The emulator’s response for an operation that is modelled and not yet built.
- Merge gate— The automated checks a change must pass to be merged: build, standards, security and fidelity.
- moto— An open source Python library and server that mocks AWS services, used here as the comparison.
References & Further Reading
- → AWS Emulator is now available— The general availability announcement of 11 November 2026
- → AWS Emulator documentation— User guide: install, configuration, security guidance
- → Fidelity report— How fidelity is measured and where the scores are published
- → aws/aws-emulator on GitHub— Source, releases, release notes and the issue directories
- → aws/aws-emulator-evaluation-suite— The test suite behind the fidelity scores
- → How autonomous AI agents built AWS Emulator— AWS’s account of the agent teams and the merge gate
- → moto documentation— The mock used as the comparison in this article
- → Terraform ephemeral resources— Background for the configuration applied in the compatibility section
- → Sample code for this article— Scripts, experiments and the CI recipe used for every result above
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.