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.
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.
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.
| Service | Local behaviour | What AWS actually does |
|---|---|---|
| S3 | Overwriting destroys the null version | Retains it as a previous version |
| DynamoDB | Secondary indexes update instantly | Indexes are eventually consistent |
| SQS | Never reordered or duplicated | Permits both, at-least-once delivery |
| Lambda | Execution role is not validated | Missing 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
- Every tutorial in the series carries an explicit section on how the local behaviour differs from real AWS. Not an appendix, not a footnote, a section you have to scroll past.
- The coverage matrix decides what is safe to teach. If an operation diverges in a way a beginner would not notice, it does not get taught as though it were production-accurate.
- I trust probes over documentation. Every one of these four is missing from the compatibility notes I started with.
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.