Four ways a cloud emulator lied to me

One of them destroys data and never raises an error. Found by hand-probing 38 operations across 16 AWS services.

A local cloud emulator is a wonderful thing right up until it is a liar. I spent a few weeks hand-probing one so that students could learn AWS without a credit card, and I came away with four cases where code passes on your laptop and fails in production. One of them loses data and does not raise a single error.

The project is an open-source tutorial series that teaches core AWS services against a local emulator. The whole pedagogical point is that a student gets real AWS practice with no account, no card, and no risk of an unexpected bill. That promise only works if I am honest about where the emulator stops telling the truth.

So rather than trusting the vendor compatibility table, I sat down and probed 38 operations across 16 services by hand, and wrote down what actually happened. Most of it matched. These four did not.

38

Operations probed

by hand, not from docs

16

AWS services

across the coverage matrix

4

Real divergences

that change program behaviour

1. S3 quietly eats your data

This is the one that made me stop and re-run the experiment three times, because I assumed I had made a mistake.

Enable versioning on a bucket that already has objects in it. Those pre-existing objects get a version id of literally null. That is not a bug, that is the S3 spec: they existed before versioning, so they have no real version, and AWS labels them null.

Now overwrite one of them. On real AWS, the null version is retained as a previous version. You can still get it back. On the emulator, it is destroyed.

PUT objectbefore versioningEnable versioningversion id = nullPUT againoverwritenull versionkept on AWS, gone locally
Same three API calls, two different endings. Nothing in the second path returns an error.

2. DynamoDB indexes are suspiciously helpful

Write an item, immediately query a global secondary index for it, and locally you will get it back every single time. Real AWS secondary indexes are eventually consistent. The propagation delay is usually small, and it is very much not zero.

the pattern that only works on your laptoppython
table.put_item(Item={"id": "42", "status": "READY"})

# Locally: returns the item, 100% of the time.
# On AWS: returns it most of the time. The rest is your pager.
resp = table.query(
    IndexName="status-index",
    KeyConditionExpression=Key("status").eq("READY"),
)

The failure mode here is worse than a straightforward crash, because it is a flake. It passes in CI, it passes in staging, it passes the first forty times in production, and then it fails under load when the write volume grows and propagation falls behind. Then somebody spends a day blaming the network.

3. SQS is too well behaved

In every local run, standard queues delivered messages in order and exactly once. Real AWS standard queues guarantee neither. They are explicitly at-least-once, with best-effort ordering.

So a consumer with no idempotency at all looks perfectly correct locally. Ship it, and the first duplicate delivery charges somebody twice.

4. Lambda does not check IAM at all

The execution role attached to a local Lambda is not validated. You can hand it a role with no permissions whatsoever and the function runs happily.

On AWS, a missing logs:CreateLogGroup does not stop your function from executing. It stops your function from logging. So the code works, and then something goes wrong later, and you go looking for the logs that explain it, and there are no logs, and there is no explanation.

The pattern behind all four

Look at these together and the common thread is not that the emulator is wrong. It is that the emulator is better than the real thing. Every one of these divergences is the local environment being stricter, faster, or more forgiving than production.

ServiceLocal behaviourWhat AWS actually does
S3Overwriting destroys the null versionRetains it as a previous version
DynamoDBSecondary indexes update instantlyIndexes are eventually consistent
SQSNever reordered or duplicatedPermits both, at-least-once delivery
LambdaExecution role is not validatedMissing permissions break logging

That direction matters. An emulator that is harsher than production gives you false failures, which are annoying but self-correcting because you go and investigate them. An emulator that is gentler than production gives you false confidence, and false confidence ships.

What I actually do about it

None of this makes the emulator less useful. I still think running real AWS APIs with no account and no card is a great way to learn. It just means "it works locally" is a statement about your laptop, and it was never a statement about production.