AlgoInsight

Free lesson · Developer on AWS

Local development against AWS - and what lies to you

Easy9 min readawscloudbackend

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 --watch and cdk watch give 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 --watch mode 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 sync or cdk watch against 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 --watch or cdk watch against 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 --watch or cdk watch against 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

Back to Developer on AWS