Lambda

Lightweight, dependency-free fake of AWS Lambda that speaks the real Lambda REST-JSON (restJson1) wire protocol, so application code using @aws-sdk/client-lambda can run against it with zero cost and zero side effects. As a bonus over a pure mock, it can actually execute simple Node.js handler source, so Invoke returns real, meaningful payloads.

KeyValue
Port4571
ProtocolAWS Lambda REST-JSON (restJson1) over HTTP
Compatible client@aws-sdk/client-lambda (v3)
Size~90 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { LambdaServer } from "./services/lambda/src/server.js";

const server = new LambdaServer(4571);
await server.start();
// ... use it ...
await server.stop();

Connect with the real AWS SDK client and run a function:

import {
  LambdaClient,
  CreateFunctionCommand,
  InvokeCommand,
} from "@aws-sdk/client-lambda";

const lambda = new LambdaClient({
  region: "us-east-1",
  endpoint: "http://127.0.0.1:4571",
  credentials: { accessKeyId: "parlel", secretAccessKey: "parlel" },
});

// A raw Node.js handler stored as the "zip" — the parlel fake executes it.
const handler = `
exports.handler = async (event, context) => {
  return { doubled: event.n * 2, fn: context.functionName };
};
`;

await lambda.send(
  new CreateFunctionCommand({
    FunctionName: "doubler",
    Runtime: "nodejs20.x",
    Role: "arn:aws:iam::123456789012:role/lambda-role",
    Handler: "index.handler",
    Code: { ZipFile: new TextEncoder().encode(handler) },
  }),
);

const res = await lambda.send(
  new InvokeCommand({
    FunctionName: "doubler",
    Payload: new TextEncoder().encode(JSON.stringify({ n: 21 })),
  }),
);

console.log(res.StatusCode); // 200
console.log(JSON.parse(new TextDecoder().decode(res.Payload))); // { doubled: 42, fn: "doubler" }

Executable handlers

Invoke actually runs your function when the code is recoverable JavaScript:

The runtime supports:

If the code is not recoverable JS (e.g. a real zip or an S3 reference), Invoke falls back to echoing the input payload (LocalStack-style), so calls still succeed.

A thrown handler error produces an Invoke response with StatusCode: 200, header X-Amz-Function-Error: Unhandled, and a JSON error payload { errorType, errorMessage, trace } — matching real Lambda.

Resetting state

State is fully in-memory and ephemeral. Reset it between tests:

server.reset();                                  // in-process
await fetch("http://127.0.0.1:4571/_parlel/reset", { method: "POST" }); // over HTTP

Health check:

await fetch("http://127.0.0.1:4571/_parlel/health");
// { status: "ok", service: "lambda", functions: <n> }

Implemented operations

Grouped by area. Every operation below is exercised in tests/lambda.test.ts.

Function lifecycle

Invocation

Versions

Aliases

Permissions / resource policy

Tags

Concurrency

Function URLs

Event source mappings

Layers

Async invoke config (retry/destinations)

Misc config

Account

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureSupportedNotes
Function CRUD + configurationFull lifecycle, validation, revision IDs
Real handler executionNode.js handler source executed in-process (async + callback styles)
Invoke (RequestResponse / Event / DryRun)Real payloads, FunctionError, ExecutedVersion, tailed logs
Versions & aliasesImmutable version snapshots; alias routing for Invoke
Resource policy (Add/Remove/GetPolicy)Statement store; JSON policy document
TagsCreate-time + TagResource/UntagResource/ListTags
Reserved & provisioned concurrencyIn-memory; provisioned requires a qualifier
Function URLsOne config per function
restJson1 error envelopeCanonical { "__type", "message" } body + x-amzn-errortype header
Async invoke config / recursion / runtime mgmtConfig stored and returned
GetAccountSettingsStatic limits + live usage counts
Event source mappingsConfig stored/returned; no polling/trigger delivery; State resolves straight to Enabled
LayersPublish/list/get/delete metadata; content not attached to the runtime
Function lifecycle State (PendingActive)Returns Active immediately — waiter-safe (waitUntilFunctionActiveV2 treats Active as success)
ListFunctions field subsetReturns the full configuration; real API omits State/StateReason from list results (extra fields are additive)
Code Signing configs⟳ Roadmap
InvokeWithResponseStream⟳ Roadmap
Real IAM / SigV4 auth enforcement⟳ Roadmap — credentials accepted, signature not verified
VPC / EFS / X-Ray side effects⟳ Roadmap
Cold starts / real timeouts / throttling⟳ Roadmap

Event source mappings and layers are stored and returned faithfully, but the fake does not poll event sources or attach layer code to the runtime — they exist so configuration-driven application code works unchanged.

Error codes / shapes

Errors use the restJson1 shape. The error code is carried in the x-amzn-errortype response header (which the SDK reads first) and in the JSON body via the __type discriminator and a lowercase message — byte-identical to the real Lambda API (no capital-Message key):

{ "__type": "ResourceNotFoundException", "message": "Function not found: arn:aws:lambda:..." }
Error codeHTTP statusWhen
ResourceNotFoundException404Function/alias/version/mapping/policy/config/layer not found
ProvisionedConcurrencyConfigNotFoundException404No provisioned concurrency config for the qualifier
ResourceConflictException409Duplicate function name, alias, statement id, or function URL
InvalidParameterValueException400Bad name, runtime, memory size, timeout, missing required input, or UpdateFunctionCode with no code source
InvalidRequestContentException400Request body is not valid JSON
PreconditionFailedException412CodeSha256 / RevisionId mismatch on PublishVersion
ValidationException400Invalid enum value (e.g. recursion config)
ServiceException500Unexpected internal error

The Invoke operation is special: a handler that throws is not a transport error. It returns HTTP 200 with the header X-Amz-Function-Error: Unhandled and a JSON error payload in the body.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

AWS_ACCESS_KEY_ID=parlel
AWS_SECRET_ACCESS_KEY=parlel
AWS_REGION=us-east-1
AWS_ENDPOINT_URL_LAMBDA=http://parlel-bridge:4571
AWS_ENDPOINT_URL=http://parlel-bridge:4571
<!-- parlel:testenv:end -->