Free lesson · Developer on AWS
Local development against AWS - and what lies to you
The Problem Worth Solving
The cloud development loop is slow. Change a line, deploy, wait, test, read logs, repeat. Minutes per iteration where local development gives you seconds.
That friction is real and it is worth attacking. The mistake is attacking it by simulating AWS locally and then trusting the simulation, because the bugs that cost the most time are precisely the ones a local environment cannot reproduce.
This topic covers the options, and then the more useful half: what each one lies to you about.
The Options
| Approach | What it is | Fidelity |
|---|---|---|
| Unit tests with mocks | The SDK stubbed - moto, aws-sdk-client-mock |
Logic only |
| Service emulators | DynamoDB Local, MinIO, and similar | High for one service's data plane |
| LocalStack | Many AWS services emulated in Docker | Varies by service |
| SAM local | Lambda in a container matching the runtime | Good for the runtime, nothing else |
| Hot-reload deploy | sam sync --watch, cdk watch |
Real AWS, seconds per iteration |
| Personal dev account | Your own account, deployed normally | Complete |
DynamoDB Local deserves specific mention as the highest-fidelity option in the list: it is the actual DynamoDB engine packaged to run locally, so query semantics, index behaviour, and error conditions match. For developing against DynamoDB it is genuinely good, with the caveat that it does not enforce IAM, throttling, or capacity.
LocalStack emulates a wide range of services. The core services - S3, DynamoDB, SQS, SNS, Lambda - work well for straightforward use. Coverage and fidelity fall off for less common services and for the edges of common ones, and the Pro tier covers more. Verify the current coverage for the specific services and operations you depend on rather than assuming.
SAM local runs your function in a container closely matching the Lambda runtime, which makes it good at exactly one thing: does this handler produce the right output for this event. Everything around the handler is absent.
What Lies To You
The section that matters. Every local approach is wrong about some of these, and the ordering here is by how much time each one costs when it bites.
graph TD
L[Local environment] --> I[**IAM** - you run as yourself<br/>or as nothing. No permissions<br/>are evaluated.]
L --> N[**Networking** - no VPC, no security<br/>groups, no endpoints, no DNS]
L --> T[**Throttling and quotas** —<br/>no limits enforced]
L --> C[**Concurrency, cold starts,<br/>timeouts** under real conditions]
L --> E[**Event source semantics** —<br/>batching, retries, ordering,<br/>partial failures]
L --> K[**KMS, encryption** —<br/>usually stubbed]
L --> V[**Service versions** - emulator<br/>behaviour lags the real API]
I --> P[**The dominant cost.**<br/>Works locally, AccessDenied<br/>in the cloud.]
This diagram lists what a local development environment does not reproduce. IAM is the largest gap: locally you run as your own credentials or none at all, and no permissions are evaluated. Networking is absent - no VPC, security groups, endpoints, or DNS. Throttling and quotas are not enforced. Concurrency, cold starts, and timeouts under real conditions do not occur. Event source semantics including batching, retries, ordering, and partial failures are approximated at best. KMS and encryption are usually stubbed. Emulator behaviour lags the real API as services change. IAM is the dominant cost, producing code that works locally and fails with access denied in the cloud.
IAM - The Expensive One
Important: Locally, your code runs with your credentials, or with fake credentials an emulator ignores. In the cloud it runs with an execution role that has exactly the permissions someone wrote down.
Every permission problem - a missing action, a resource ARN that does not match, a KMS key the role cannot use, a condition that does not evaluate as expected - is invisible until deployment. And these are not rare edge cases; permission errors are the single largest category of "it worked locally" failure, because the thing you tested against had no permission model at all.
The corollary is that a local test suite passing tells you nothing about whether the code will run. It tells you the logic is right, which is worth having and is not the same claim.
Networking
VPC configuration, security groups, route tables, VPC endpoints, and private DNS do not exist locally. A function that must reach a database in a private subnet, or an AWS service through an endpoint, works locally because your laptop has ordinary internet access.
The characteristic failure is Course 1 Module 04 Topic 11's: a VPC-attached Lambda with no NAT and no endpoint hangs until timeout with no useful error. Local testing cannot surface this because there is no VPC.
Timing, Consistency, And Limits
Emulators are frequently synchronous where the real service is not, and they enforce no quotas. Code that works locally because an operation completed immediately, or because nothing throttled it, meets different behaviour under real conditions.
Cold starts, concurrency limits, and real timeouts are similarly absent - a function that takes 400 ms locally may take considerably longer on its first invocation in a cold environment.
Event Source Semantics
The behaviour of an event source mapping - batch composition, partial batch failure handling, retry counts, ordering guarantees, visibility timeouts, poison record handling - is approximated at best by local tooling.
For anything consuming SQS, Kinesis, DynamoDB Streams, or Kafka, this is where the real complexity lives (Course 1 Module 07), and it is precisely what local testing does not exercise.
The Better Fast Loop
The framing that resolves most of this: the goal is a fast iteration loop, not a local one. Those are different requirements and conflating them is what leads to elaborate local environments.
Tip:
sam sync --watchandcdk watchgive you a few seconds per iteration against real AWS. They detect changes and push code updates directly, skipping the full CloudFormation deployment for code-only changes.You get real IAM, real networking, real service behaviour, real event sources - and an iteration speed close to local. For most serverless development this is simply better than a local emulator on both axes, and it is under-adopted because "local development" is the assumed goal rather than "fast feedback."
The constraint is that it is a development-account tool. As Course 1 Module 09 Topic 5 notes, it deliberately bypasses CloudFormation, so the deployed state diverges from the stack. That is fine in a personal stack and unacceptable anywhere shared. The
--watchmode makes it very easy to leave running against the wrong profile - which is Topic 1's argument for having no default profile.
Per-developer sandbox accounts are the team-scale version. Each developer gets an account, vended with a baseline and a budget (Course 1 Module 10 Topic 11), and deploys their own stack. Cost is controlled by the budget and by tearing down; isolation is complete; and the environment is real.
The objection is cost, and it is usually smaller than expected - a developer's sandbox running serverless resources with a few test invocations costs very little, and the engineering time saved by not debugging emulator differences is not close.
What Local Is Genuinely Good For
Not a rejection of local tooling - a scoping of it.
Unit testing business logic. Pure functions, transformations, validation, calculations. Fast, deterministic, no AWS at all. This should be the bulk of your test suite and it needs no emulator - just the SDK mocked at the boundary.
Testing SDK interaction shapes. Libraries like moto and aws-sdk-client-mock let you assert that your code calls the right operation with the right parameters, and handles the responses and errors correctly. That is a real contract worth testing and it runs in milliseconds.
Iterating on a handler's logic. sam local invoke with a fixed event, when you are working on what the function computes rather than what it integrates with.
Developing against DynamoDB. DynamoDB Local's fidelity is high enough that data-plane development is genuinely reliable there.
Working offline. Occasionally the actual requirement.
A Testing Strategy That Works
| Layer | Where | Tests |
|---|---|---|
| Unit | Local, SDK mocked | Logic, transformations, error handling |
| Component | Local, emulator or mock | The code's interaction with one service |
| Integration | A real dev account | IAM, networking, event sources, real service behaviour |
| End-to-end | A shared dev or staging account | The whole path, with real dependencies |
Warning: The integration layer is the one teams skip, and it is the one that catches the expensive bugs. A test suite that is entirely local and entirely green gives a confidence that is not warranted, because the categories it cannot test - permissions, networking, event source behaviour - are where production failures come from.
At minimum, run integration tests against a real account in CI on every merge. With OIDC (Course 1 Module 09 Topic 12) this needs no stored credentials, and it is the difference between finding a missing IAM action in a pipeline and finding it in production.
Choosing
| Situation | Use |
|---|---|
| Testing a pure function | Unit test, no AWS |
| Testing SDK call shapes and error handling | Mocking library |
| Developing DynamoDB access patterns | DynamoDB Local |
| Iterating on handler logic | sam local invoke |
| Iterating on anything integrated | sam sync --watch or cdk watch against a dev stack |
| Validating IAM, networking, event sources | A real dev account |
| Working offline | LocalStack or emulators, with known limitations |
| CI on every merge | Unit and component locally, integration against a real account |
Common Failure Modes
Warning: Trusting local testing for permissions. The dominant source of works-locally failures.
No dev account. Nowhere to test what local cannot.
An entirely local test suite. Green, and not evidence the code runs.
Building an elaborate local environment instead of adopting a fast deploy loop.
sam syncorcdk watchagainst a shared or production stack. Deliberate drift.Emulator behaviour assumed current. It lags the real API.
Event source semantics assumed from local behaviour. Batching and retries differ.
VPC-attached functions tested only locally. No VPC exists locally.
LocalStack Pro cost unmodelled at team scale.
Integration tests not in CI. Found in production instead of in a pipeline.
Local and deployed configuration diverging. Different environment variables, different behaviour.
Why This Matters In An Interview
Interview: This question is really about testing judgement, and the strong answer scopes local development rather than defending or dismissing it.
What interviewers are listening for:
- IAM as the category local cannot test, named specifically.
- A fast loop against real AWS as preferable to a local simulation.
- Integration tests against a real account in CI.
- Local used for logic, not for integration.
Common follow-ups:
- "It works locally and fails when deployed. What are the likely causes?" → IAM permissions first, because local evaluates none. Then VPC networking. Then event source or timing behaviour. In that order, because that is the frequency order.
- "How would you speed up a slow serverless development loop?" →
sam sync --watchorcdk watchagainst a personal dev stack. Seconds per iteration with real IAM, networking, and event sources. Better than a local emulator on both speed and fidelity, provided the stack is personal.- "Is LocalStack worth using?" → For offline work and for fast component tests of well-supported services, yes. Not as the primary integration environment, because the things it cannot reproduce are the things that break in production. Scope it rather than rejecting it.
- "What does your test suite look like for a serverless application?" → Mostly unit tests of logic with the SDK mocked, component tests against emulators or mocks, and integration tests against a real dev account running in CI on every merge. The last layer is the one that catches permissions and networking.
Practice
Exercise: Starter. Take a Lambda function that passes a local test and deploy it with an execution role granting nothing. Observe the failure, and note that no local test could have caught it.
Intermediate. Set up
sam sync --watchorcdk watchagainst a personal dev stack. Measure the iteration time and compare it against your current local loop and against a full deploy.Advanced. Write the three test layers for one function: a unit test of the logic with the SDK mocked, a component test against an emulator, and an integration test against a real dev account exercising IAM and an event source. Note which layer catches which class of bug.
Related Topics
- Topic 1 - The dev account this depends on
- Topic 2 - The credentials chain, which local testing does not exercise
- Course 1 Module 04 Topic 11 - Lambda in a VPC, and why local cannot test it
- Course 1 Module 07 - Event source semantics that local approximates
- Course 1 Module 09 Topic 5 - SAM local and
sam syncin detail - Course 1 Module 09 Topic 12 - OIDC for integration tests in CI