DEV Community

Cover image for AWS Emulator: Test Serverless Code Locally Without an Account
Sascha Mirow-Stanitzok
Sascha Mirow-Stanitzok

Posted on Originally published at sascha-ms.de

AWS Emulator: Test Serverless Code Locally Without an Account

The AWS Emulator has been available since November 11, and it hits a nerve that has followed me through serverless projects for years: how do I test code that consists almost entirely of AWS calls without deploying for every test run? Until now the answer was usually “mocks” or “a test account in the cloud”. Mocks are fast, but they only check whether you rebuilt your own assumptions correctly. A real account is honest, yet slow, and in a CI pipeline you need credentials for it at the latest.

Now AWS ships a local emulator for its own APIs. It is free, needs no AWS account and comes with a license that sets practically no limits. In this article we look at what it can do and where it stops. After that you build, step by step, an integration test for a Lambda handler that runs against the emulator: locally and in GitHub Actions.

In short

The AWS Emulator is an official local replica of the AWS APIs, and you point your unchanged SDK code at it through environment variables. For integration tests with DynamoDB, SQS and friends, this closes a real gap. The biggest pitfall is expectations: it does not check IAM policies, and how closely it matches AWS depends on the service.

What you will learn in this article

  • What the AWS Emulator is and what it deliberately does not do
  • How to start it with Docker or a Linux package and point your SDK at it
  • An integration test for a Lambda handler with DynamoDB and SQS in Vitest
  • A safety net that keeps tests away from real AWS accounts
  • How to use the emulator in GitHub Actions

What is the AWS Emulator?

The AWS Emulator is a command-line program without a user interface. It serves a local HTTPS endpoint that behaves like the AWS APIs. By default it listens on https://127.0.0.1:9297. Your code stays the same: the AWS CLI and SDKs read the endpoint from the environment variable AWS_ENDPOINT_URL and send their calls there instead of to the cloud.

A few properties make it interesting. The services are generated from the same API models AWS uses to build the SDKs. New APIs therefore show up automatically, and their behavior is filled in release by release. For supported operations, the emulator aims to return the same responses and the same errors as AWS. Operations that are not implemented yet get an honest 501 Not Implemented instead of some made-up answer.

In addition, it keeps all resources in memory. When it stops, everything is gone, and every restart begins clean. Each instance gets its own port, its own keys and its own state. As a result, parallel CI jobs or several AI agents do not get in each other’s way. The source code is on GitHub under CC0 1.0, which is practically public domain. There are no extra fees.

What does “fidelity” mean?

AWS calls the accuracy of the replica fidelity. It is measured with fidelity tests: short programs that run a workflow with an AWS SDK against real AWS and record results, state and error messages. A test only passes for the emulator if every call answers exactly the same way. AWS publishes the pass rate per service with every release on GitHub. According to the documentation, the match is currently highest for control plane APIs (creating, changing and deleting resources). For data plane calls and for infrastructure-as-code tools such as CloudFormation or CDK, it varies by service.

Where the AWS Emulator fits in your test strategy

The emulator replaces neither unit tests nor tests in a real account. It fills the layer in between, which is often missing in serverless projects. This is how I see the split:

Three test layers: unit tests with mocks at the bottom, integration against the AWS Emulator in the middle, end-to-end on top

The AWS Emulator fills the gap between unit tests and tests in a real account.

Unit tests with mocks remain the foundation. They check logic in milliseconds, for example input validation. I described what that looks like with Vitest in Lambda unit tests with Vitest. Integration tests against the emulator then check whether your SDK calls really work: whether the key matches the table, whether the condition in the PutItem kicks in, whether the message lands in the queue in the right format. At the top sit a few end-to-end tests in a real account, because only there do you see IAM, limits and how the triggers interact.

What the emulator cannot do

Before you start, take an honest look at the limits. Most of them are stated openly in the documentation:

  • No IAM checks. IAM policies and resource-based policies are not enforced as they would be in AWS. Whether your Lambda role has the right permissions, you only find out in a real account.
  • One account, one Region. Any Region and account ID are accepted, yet resources are not kept apart by them. For several Regions, start several instances on different ports.
  • Memory only. After stopping, everything is gone. For tests that is an advantage, for a local development environment with data less so.
  • Not built for long-running use. Load and long-running operation have not been tested. Start it for a test run and stop it afterwards, or set --idle-timeout.
  • Fidelity varies. How well a service is replicated is listed in the release notes. Check this for the services your project actually uses.

There is one more point the getting-started docs leave open: whether and how the emulator runs Lambda functions themselves. For this example it does not matter. I call the handler directly as a function in the test and only let the AWS calls inside it go to the emulator. That is fast, easy to debug and tests exactly the part that would otherwise only be covered by mocks.

Step by step: test a Lambda handler against the AWS Emulator

The example is a fictional bike rental. A Lambda function behind an HTTP API accepts reservations. It stores them in DynamoDB and then sends an event to SQS so that other parts of the system can react. One rule matters: a bike can only be booked once per day. This kind of rule is hard to test with mocks, because the condition is evaluated in DynamoDB and not in your code.

Prerequisites

  • Node.js 22 or later and npm
  • Docker (macOS, Windows) or Linux with dnf or apt
  • Optionally the AWS CLI v2 to try things out
  • No AWS account, no credentials

Step 1: Install and start the emulator

On Linux, install the official package and start the emulator directly. For Debian and Ubuntu it looks like this:

curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
sudo apt-get install -y ./aws-emulator.deb
aws-emulator -v
aws-emulator --telemetry off
Enter fullscreen mode Exit fullscreen mode

On macOS and Windows it runs in a container. AWS does not provide a ready-made image for this, but a short Dockerfile that you build yourself:

# docker/Dockerfile – based on the AWS Emulator user guide
# On Apple silicon or other Arm machines use aws-emulator-aarch64.rpm
FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal
ADD https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm /tmp/aws-emulator.rpm
RUN rpm -i /tmp/aws-emulator.rpm && rm /tmp/aws-emulator.rpm \
    && mkdir -p /home/emulator && chown 1000 /home/emulator
USER 1000
ENV HOME=/home/emulator
EXPOSE 9297
ENTRYPOINT ["aws-emulator", "-b", "0.0.0.0"]
Enter fullscreen mode Exit fullscreen mode
docker build -t aws-emulator docker/
mkdir -p ~/.aws/emulator
PORT=9297
docker run --rm --memory=2g --memory-swap=2g -p 127.0.0.1:$PORT:$PORT \
  -e HOME="$HOME" -v "$HOME/.aws/emulator:$HOME/.aws/emulator" \
  aws-emulator -p $PORT --telemetry off
Enter fullscreen mode Exit fullscreen mode

Pay attention to 127.0.0.1 in front of the port. The short form -p 9297:9297 would make the emulator reachable from your whole network. The mount of ~/.aws/emulator matters as well: on every start, the emulator writes credentials, a config file and its self-signed certificate there, each in a subfolder named after the port. With --telemetry off you turn off the anonymous usage data, which is on by default.

Step 2: Point your shell at the emulator

Now you tell the CLI and SDK where to connect. I put this in a small script that you load with source. The variables come from the documentation. For the JavaScript SDK on Node.js, NODE_EXTRA_CA_CERTS is added, because that is how Node.js trusts the emulator’s self-signed certificate.

# scripts/emulator-env.sh
# Usage: source scripts/emulator-env.sh [port]
PORT="${1:-9297}"
EMU_DIR="$HOME/.aws/emulator/$PORT"

# Never mix real credentials with the emulator.
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE

export AWS_ENDPOINT_URL="https://127.0.0.1:$PORT"
export AWS_SHARED_CREDENTIALS_FILE="$EMU_DIR/emulator_credentials"
export AWS_CONFIG_FILE="$EMU_DIR/config"
export AWS_CA_BUNDLE="$EMU_DIR/ca.pem"
export NODE_EXTRA_CA_CERTS="$EMU_DIR/ca.pem"
export AWS_EC2_METADATA_DISABLED=true
export AWS_DISABLE_HOST_PREFIX_INJECTION=true
export AWS_REGION=us-east-1
export AWS_DEFAULT_REGION=us-east-1
echo "Shell now points at the AWS Emulator on port $PORT"
Enter fullscreen mode Exit fullscreen mode

Afterwards you can check whether everything arrives. The account 000000000000 and the user emulator tell you that you are talking to the emulator. If you see your real account ID, the calls are going to AWS.

source scripts/emulator-env.sh
aws sts get-caller-identity
aws dynamodb list-tables
Enter fullscreen mode Exit fullscreen mode

Step 3: Set up the project

The project only needs the SDK clients for DynamoDB and SQS, plus Vitest and TypeScript. The test uses the STS client for the safety net from step 5.

{
  "name": "aws-emulator-local-tests",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "emulator:check": "bash scripts/wait-for-emulator.sh",
    "test:int": "vitest run --config vitest.config.ts"
  },
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.1167.0",
    "@aws-sdk/client-sqs": "^3.1167.0"
  },
  "devDependencies": {
    "@aws-sdk/client-sts": "^3.1167.0",
    "@types/aws-lambda": "^8.10.145",
    "@types/node": "^22.10.0",
    "typescript": "^5.9.3",
    "vitest": "^3.2.4"
  }
}
Enter fullscreen mode Exit fullscreen mode
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src", "test", "vitest.config.ts"]
}
Enter fullscreen mode Exit fullscreen mode
// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["test/**/*.int.test.ts"],
    globalSetup: ["test/global-setup.ts"],
    testTimeout: 15_000,
  },
});
Enter fullscreen mode Exit fullscreen mode

Step 4: The handler

The handler is ordinary Lambda code. It knows nothing about the emulator, and that is exactly the point. A ConditionExpression prevents double bookings: if an item already exists for that bike and day, DynamoDB throws a ConditionalCheckFailedException, and the handler answers with 409.

// src/reservation.ts
import {
  ConditionalCheckFailedException,
  DynamoDBClient,
  PutItemCommand,
} from "@aws-sdk/client-dynamodb";
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";

// No emulator-specific code: endpoint, credentials and region come from the environment.
const dynamo = new DynamoDBClient({});
const sqs = new SQSClient({});

type ReservationRequest = { bikeId: string; day: string; customerId: string };

function parse(body: string | undefined): ReservationRequest | undefined {
  try {
    const data = JSON.parse(body ?? "{}");
    const valid =
      typeof data.bikeId === "string" &&
      typeof data.customerId === "string" &&
      /^\d{4}-\d{2}-\d{2}$/.test(data.day ?? "");
    return valid ? data : undefined;
  } catch {
    return undefined;
  }
}

const json = (statusCode: number, body: unknown): APIGatewayProxyResultV2 => ({
  statusCode,
  headers: { "content-type": "application/json" },
  body: JSON.stringify(body),
});

export async function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> {
  const request = parse(event.body);
  if (!request) {
    return json(400, { message: "bikeId, customerId and day (YYYY-MM-DD) are required" });
  }

  try {
    // One bike per day: the condition prevents double bookings.
    await dynamo.send(
      new PutItemCommand({
        TableName: process.env.TABLE_NAME,
        Item: {
          pk: { S: `BIKE#${request.bikeId}` },
          sk: { S: `DAY#${request.day}` },
          customerId: { S: request.customerId },
          createdAt: { S: new Date().toISOString() },
        },
        ConditionExpression: "attribute_not_exists(pk)",
      }),
    );
  } catch (error) {
    if (error instanceof ConditionalCheckFailedException) {
      return json(409, { message: "Bike is already booked on this day" });
    }
    throw error;
  }

  await sqs.send(
    new SendMessageCommand({
      QueueUrl: process.env.QUEUE_URL,
      MessageBody: JSON.stringify({ type: "ReservationCreated", ...request }),
    }),
  );

  return json(201, { bikeId: request.bikeId, day: request.day });
}
Enter fullscreen mode Exit fullscreen mode

Step 5: A safety net against real accounts

Integration tests create tables and queues and delete them again. If such a test accidentally runs with real credentials, all of that ends up in your AWS account. That is why a global setup checks who answers before any test runs. If it is not the emulator account, Vitest stops immediately. It costs one call and may save you an unpleasant surprise.

// test/global-setup.ts
import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";

// Safety net: the tests create and delete resources.
// They must only ever run against the emulator, never against a real account.
export default async function setup(): Promise<void> {
  if (!process.env.AWS_ENDPOINT_URL) {
    throw new Error("AWS_ENDPOINT_URL is not set – source scripts/emulator-env.sh first");
  }
  const expected = process.env.EMULATOR_ACCOUNT_ID ?? "000000000000";
  const { Account } = await new STSClient({}).send(new GetCallerIdentityCommand({}));
  if (Account !== expected) {
    throw new Error(`Expected emulator account ${expected}, got ${Account}. Aborting.`);
  }
}
Enter fullscreen mode Exit fullscreen mode

You only need the variable EMULATOR_ACCOUNT_ID if you want to run the same tests against another emulator with a different account ID. The tests use nothing but standard SDK calls, so they are not tied to one tool.

Step 6: The integration test

For each run, the test creates its own table and its own queue with a random suffix. Then it calls the handler directly and reads the result back through the SDK. Three cases are enough to start with: the successful booking, the double booking and an invalid input.

// test/reservation.int.test.ts
import { randomUUID } from "node:crypto";
import {
  CreateTableCommand,
  DeleteTableCommand,
  DynamoDBClient,
  GetItemCommand,
  waitUntilTableExists,
} from "@aws-sdk/client-dynamodb";
import {
  CreateQueueCommand,
  DeleteQueueCommand,
  ReceiveMessageCommand,
  SQSClient,
} from "@aws-sdk/client-sqs";
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { handler } from "../src/reservation.js";

const dynamo = new DynamoDBClient({});
const sqs = new SQSClient({});
// Unique names per run, so parallel test files do not collide.
const suffix = randomUUID().slice(0, 8);
const tableName = `bike-reservations-${suffix}`;
let queueUrl = "";

const request = (body: unknown) => ({ body: JSON.stringify(body) }) as APIGatewayProxyEventV2;

async function callHandler(body: unknown): Promise<APIGatewayProxyStructuredResultV2> {
  return (await handler(request(body))) as APIGatewayProxyStructuredResultV2;
}

beforeAll(async () => {
  await dynamo.send(
    new CreateTableCommand({
      TableName: tableName,
      BillingMode: "PAY_PER_REQUEST",
      AttributeDefinitions: [
        { AttributeName: "pk", AttributeType: "S" },
        { AttributeName: "sk", AttributeType: "S" },
      ],
      KeySchema: [
        { AttributeName: "pk", KeyType: "HASH" },
        { AttributeName: "sk", KeyType: "RANGE" },
      ],
    }),
  );
  await waitUntilTableExists({ client: dynamo, maxWaitTime: 30 }, { TableName: tableName });
  const queue = await sqs.send(new CreateQueueCommand({ QueueName: `reservation-events-${suffix}` }));
  queueUrl = queue.QueueUrl!;
  process.env.TABLE_NAME = tableName;
  process.env.QUEUE_URL = queueUrl;
});

afterAll(async () => {
  await dynamo.send(new DeleteTableCommand({ TableName: tableName }));
  await sqs.send(new DeleteQueueCommand({ QueueUrl: queueUrl }));
});

describe("create reservation", () => {
  it("stores the reservation and publishes an event", async () => {
    const result = await callHandler({ bikeId: "e-42", day: "2026-11-20", customerId: "c-7" });
    expect(result.statusCode).toBe(201);

    const item = await dynamo.send(
      new GetItemCommand({
        TableName: tableName,
        Key: { pk: { S: "BIKE#e-42" }, sk: { S: "DAY#2026-11-20" } },
      }),
    );
    expect(item.Item?.customerId?.S).toBe("c-7");

    const messages = await sqs.send(
      new ReceiveMessageCommand({ QueueUrl: queueUrl, WaitTimeSeconds: 1, MaxNumberOfMessages: 1 }),
    );
    expect(JSON.parse(messages.Messages?.[0]?.Body ?? "{}")).toMatchObject({
      type: "ReservationCreated",
      bikeId: "e-42",
    });
  });

  it("rejects a double booking with 409", async () => {
    await callHandler({ bikeId: "e-43", day: "2026-11-21", customerId: "c-1" });
    const second = await callHandler({ bikeId: "e-43", day: "2026-11-21", customerId: "c-2" });
    expect(second.statusCode).toBe(409);
  });

  it("returns 400 for an invalid day", async () => {
    const result = await callHandler({ bikeId: "e-44", day: "tomorrow", customerId: "c-3" });
    expect(result.statusCode).toBe(400);
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 7: Run the tests

Start the emulator in one terminal and run the tests in a second one. The wait script polls the health route until the emulator is ready:

#!/usr/bin/env bash
# scripts/wait-for-emulator.sh – waits up to 30 seconds for the health route
PORT="${1:-9297}"
CA="$HOME/.aws/emulator/$PORT/ca.pem"
for i in $(seq 30); do
  if [ -f "$CA" ] && curl -sf --cacert "$CA" "https://127.0.0.1:$PORT/__admin/health" > /dev/null; then
    echo "Emulator is ready on port $PORT"
    exit 0
  fi
  sleep 1
done
echo "Emulator did not respond on port $PORT" >&2
exit 1
Enter fullscreen mode Exit fullscreen mode
npm install
bash scripts/wait-for-emulator.sh
source scripts/emulator-env.sh
npm run test:int
Enter fullscreen mode Exit fullscreen mode

If everything works, the output looks roughly like this:

 ✓ test/reservation.int.test.ts (3 tests)

 Test Files  1 passed (1)
      Tests  3 passed (3)
Enter fullscreen mode Exit fullscreen mode

If you get an error with NotImplemented instead, the emulator does not support one of the operations yet. In that case, check the release notes. You can also report the gap in the emulator’s GitHub repository, although AWS does not accept pull requests there.

🎬 Flow of an integration test against the AWS Emulator – this animated diagram only runs on my site: watch it on sascha-ms.de

Using the AWS Emulator in GitHub Actions

The CI pipeline is where the emulator shines. You need no test account, no OIDC role and no secrets. On a Linux runner you install the Debian package, start the emulator in the background and wait for the health route. With --idle-timeout it stops itself if a job hangs.

# .github/workflows/integration-tests.yml
name: integration-tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - name: Install AWS Emulator
        run: |
          curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
          sudo apt-get install -y ./aws-emulator.deb
          aws-emulator -v
      - name: Start AWS Emulator
        run: |
          aws-emulator --telemetry off --idle-timeout 900 > emulator.log 2>&1 &
          bash scripts/wait-for-emulator.sh
      - run: npm ci
      - name: Run integration tests
        run: |
          source scripts/emulator-env.sh
          npm run test:int
Enter fullscreen mode Exit fullscreen mode

A note on the pipeline: the emulator is not meant for testing security. A green run tells you that your logic and your SDK calls are correct, not that your IAM role fits. For that you still have template tests (see Testing the Serverless Framework) and a deployment to a test environment.

The same handler in a real account

On the way to the cloud, nothing changes in the handler. The serverless.yml creates the table and the queue, passes their name and URL as environment variables and gives the function exactly the two permissions it needs. These permissions are precisely what the emulator cannot check for you, so they are deliberately short and readable.

# serverless.yml – deploys the same handler to real AWS (Serverless Framework v4)
service: bike-rental-reservations
frameworkVersion: "4"

provider:
  name: aws
  runtime: nodejs22.x
  architecture: arm64
  region: eu-central-1
  environment:
    TABLE_NAME: !Ref ReservationsTable
    QUEUE_URL: !Ref ReservationEventsQueue
  iam:
    role:
      statements:
        - Effect: Allow
          Action: dynamodb:PutItem
          Resource: !GetAtt ReservationsTable.Arn
        - Effect: Allow
          Action: sqs:SendMessage
          Resource: !GetAtt ReservationEventsQueue.Arn

functions:
  createReservation:
    handler: src/reservation.handler
    events:
      - httpApi:
          method: POST
          path: /reservations

resources:
  Resources:
    ReservationsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: pk
            AttributeType: S
          - AttributeName: sk
            AttributeType: S
        KeySchema:
          - AttributeName: pk
            KeyType: HASH
          - AttributeName: sk
            KeyType: RANGE
    ReservationEventsQueue:
      Type: AWS::SQS::Queue
Enter fullscreen mode Exit fullscreen mode

Common mistakes with the AWS Emulator

Certificate errors in Node.js

If Node.js reports self-signed certificate, NODE_EXTRA_CA_CERTS is missing. The variable has to be set before the Node process starts. Also, the emulator creates a new certificate on every start. The path stays the same, while a copied file is outdated right away.

“localhost” instead of 127.0.0.1

If you set AWS_ENDPOINT_URL to https://localhost:9297, the JavaScript, Java and Go SDKs move the S3 bucket name into the host name. The emulator does not understand that. Stick with the IP address, and the bucket ends up in the path.

Real credentials win

Environment variables such as AWS_ACCESS_KEY_ID or AWS_PROFILE take precedence over the emulator’s credentials file. That is why the script from step 2 removes them first. The safety net from step 5 catches the rest.

Host prefixes for some operations

Some operations add a prefix to the host name, for example StartSyncExecution in Step Functions. In the JavaScript SDK you turn this off per client with disableHostPrefix: true. Only set it in tests: against real AWS, exactly these operations would fail otherwise.

My take

I consider the AWS Emulator one of the most useful announcements of the year for serverless teams. Local replicas of AWS have existed for years, LocalStack or moto for example. What is new is that AWS maintains one itself, generates it from the official API models and measures the deviations in public. For me, this fidelity report is the real step forward: I no longer have to guess how close an emulator is to reality.

Still, I would not overestimate it. The biggest surprises in serverless systems rarely come from a single PutItem. They come from missing permissions, from limits, from the retry behavior of event sources and from timeouts. You only see all of that in a real account. The emulator moves the point at which you have to deploy a good deal further back. It cannot remove it.

I also find it remarkable how openly AWS names the target audience: AI coding agents. There is a dedicated skill that lets an agent install and start the emulator and check whether your project works with it. That matches what I see day to day: agents need a fast, safe environment where they can try out code without touching a real account. An emulator with isolated state per instance is built for exactly that.

Local testing is one side of the coin. If you want to run untrusted or AI-generated code in isolation in the cloud, my article Lambda MicroVMs: run AI code safely in a sandbox shows how to build a sandbox for it.

FAQ

Does the AWS Emulator cost anything?

No. There is no additional service fee, and you do not need an AWS account. Costs only arise when you actually deploy resources to AWS.

Does the AWS Emulator run on macOS and Windows?

Yes, via Docker. AWS provides a short Dockerfile from which you build the image yourself. On Linux there are also RPM and Debian packages for x86_64 and arm64.

Does the AWS Emulator check IAM permissions?

No. Permissions from IAM policies or resource-based policies are not enforced like in AWS. The emulator is explicitly not meant for security testing.

Is my data kept after a restart?

No. The emulator keeps everything in memory. When it stops, all resources are deleted, and every start begins with an empty state.

Do I have to change my code for the AWS Emulator?

Usually not. You set environment variables such as AWS_ENDPOINT_URL, the credentials file and the certificate. Only for a few operations with a host prefix do you also need disableHostPrefix in the JavaScript SDK.

Your next step

Pick a Lambda function from your project whose tests only use mocks today. Check the emulator’s release notes to see whether the services behind it are well covered, and write a single integration test following the pattern above. Take the safety net with the STS call along right away.

I am curious: which AWS services would you test against the emulator first, and where do you still trust only a real account?

All code from this article, ready to run: smirowstanitzok/sascha-ms-examples

Sources

This post was created with AI assistance.


👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.

📬 Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)