Lambda
MiniStack stores functions and answers the Lambda API. Each invocation runs a Node or Python
handler in an execution environment: a child process in Node, or a module Web Worker in a
page. Create and invoke functions with the ordinary SDK. The API behaves as
MiniStack’s Lambda page describes, except its Docker
settings, such as LAMBDA_STRICT, which don’t apply. Inside an invocation, a handler runs as it
would on AWS Lambda, except where this page says otherwise.
import { clientConfig, createRegion } from 'pocket-region/browser';import { LambdaClient, CreateFunctionCommand, InvokeCommand } from '@aws-sdk/client-lambda';import { strToU8, zipSync } from 'fflate';
const region = await createRegion();const lambda = new LambdaClient(clientConfig(region));
const code = 'export const handler = async (event) => `hello, ${event.name}`;';await lambda.send(new CreateFunctionCommand({ FunctionName: 'hello', Runtime: 'nodejs22.x', Handler: 'index.handler', Role: 'arn:aws:iam::000000000000:role/lambda', Code: { ZipFile: zipSync({ 'index.mjs': strToU8(code) }) },}));
const { Payload } = await lambda.send(new InvokeCommand({ FunctionName: 'hello', Payload: '{ "name": "world" }' }));console.log(Payload.transformToString());
await region.stop();The deployment package is any zip. This one is built with fflate.
Runtimes and packages
Section titled “Runtimes and packages”nodejs* and python* runtimes run. provided.* functions fail each invocation with a
Runtime.HandlerError naming the runtime. Java, .NET, and Ruby functions answer MiniStack’s own
200 “Mock response” without running.
| Node | Page | |
|---|---|---|
Handler: 'index.handler' loads |
index.mjs, index.js, index.cjs, or index.py |
index.mjs, index.js, or index.py |
| CommonJS | Works, with exports read from default |
Doesn’t work |
| Relative imports | Work | Work, between the package’s ES modules. export … from and circular imports don’t |
Bare imports, such as @aws-sdk/client-s3 |
Resolve from your project’s node_modules, the nearest one above the working directory |
Load from where the region’s resolve says: by default the page’s import map, else jsDelivr. SDK clients get the region as their defaults |
| Imports from a full URL | Work | Work |
| Parent environment variables | Only PATH |
None |
Lambda preinstalls the AWS SDK. A page loads it for a Node handler, and Node finds it in your project, so a handler that imports it bare and creates a client with no options runs in either as written.
Python
Section titled “Python”Every python* runtime runs Python 3.14, no matter what version the runtime’s name gives.
Pyodide ships a single Python version with each release,
and Pocket Region pins the release with 3.14. Each execution environment boots its own
interpreter, separate from the one that runs the region. The
handler is Lambda’s: Handler: 'src/app.handler' loads src/app.py with the package root on
sys.path, context has aws_request_id, function_name, and
get_remaining_time_in_millis(), an exception is logged as [ERROR] ValueError: ... with its
traceback, and a handler that can’t be imported fails with Lambda’s own message.
The environment preinstalls boto3 at the version this release was built with. Its wheels are
fetched from PyPI and Pyodide’s CDN the first time a Python function runs: 16 MB, which Node keeps
under ~/.cache/pocket-region (or $XDG_CACHE_HOME/pocket-region) and a page leaves to the
browser’s cache. A cold start is about 2.3 s in Node, as the Init Duration of a first
invocation. Other packages go in the deployment package, as on Lambda, and must be pure Python:
Pyodide can’t load a wheel built for Linux. boto3 reaches the region through fetch, with
buckets in the path, and nothing else reaches anywhere: requests, urllib, and sockets don’t
work.
What a handler sees
Section titled “What a handler sees”Lambda’s standard AWS_LAMBDA_* variables are set, along with the function’s own
Environment.Variables, which win. These point the handler at the region:
| Variable | Value |
|---|---|
AWS_ENDPOINT_URL |
The region: http://127.0.0.1:<port> in Node, http://localhost:<port> in a page |
AWS_REGION, AWS_DEFAULT_REGION |
us-east-1 |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN |
The function’s execution role, as a session of it. Where the region enforces IAM, a handler’s own calls are limited by that role’s policies; otherwise they are the account’s root user, allowed everything |
AWS_EXECUTION_ENV |
AWS_Lambda_ and the runtime, such as AWS_Lambda_python3.13 |
So an SDK client created inside a handler with no options reaches the region, as it would on
Lambda, and so does a boto3 client. In a page, process.env is provided for these, and fetch
calls to the region’s port on localhost, 127.0.0.1, or [::1] are routed to the region.
Every other fetch goes out as normal. One exception: the SDK’s browser build reads no environment, so a page handler that
bundles the SDK rather than importing it bare must pass region, endpoint, credentials, and
forcePathStyle from process.env itself.
Limits
Section titled “Limits”| Behaviour | |
|---|---|
Timeout |
Past it, the invocation fails with Runtime.ExitError, and that environment is stopped rather than reused |
| Concurrency | ReservedConcurrentExecutions per function, and MiniStack’s account cap across all of them, measured at 97 in Node and in a page |
| An idle environment | Stopped after 60 s |
| A code or configuration change | New invocations get fresh environments. The old ones finish what they’re running |
Asynchronous invocations
Section titled “Asynchronous invocations”A failed Event invocation is retried twice by default, or up to the function’s
MaximumRetryAttempts, with backoff of 1 s then 2 s, capped at 30 s, rather than AWS’s minutes.
When retries run out, the event goes to the OnFailure destination or the DeadLetterConfig
target. SQS targets are tested, SNS targets aren’t.
In Node, an asynchronous invocation runs only while something keeps Node running. A script that
ends right after an Event invocation may exit before the handler runs. See
the region.
An EventBridge rule that targets a function runs it once, as MiniStack does, with no retries.
Event source mappings
Section titled “Event source mappings”MiniStack’s poller hands a function batches from SQS queues, Kinesis streams, and DynamoDB
Streams, checking every second while a mapping exists. A stream mapping runs one batch at a time.
An SQS mapping runs as many batches at once as the function’s concurrency allows, taking the next
as each finishes, which MiniStack doesn’t do yet. All three sources are tested.
ReportBatchItemFailures is tested on SQS, where a handler’s batchItemFailures stay on the
queue. As in MiniStack, stream mappings ignore it and move past the whole batch.
Watching functions run
Section titled “Watching functions run”Pass lambda to createRegion:
type LambdaEnvironment = { functionName: string; environment: string };type LambdaOutput = LambdaEnvironment & { text: string };
type LambdaObserver = { onOutput?(output: LambdaOutput): void; onEvent?(event: LambdaEvent): void;};environment names one execution environment by its log stream. onOutput gets every line a
handler writes as text, including the START, END, and REPORT lines. The region’s own
onOutput hears the same lines as stdout, without the function’s name. onEvent gets a LambdaEvent: the LambdaEnvironment fields, plus one of:
kind |
phase |
Fields |
|---|---|---|
environment |
started, stopped |
reason when stopped |
environment |
spawned |
pid of the environment’s process (Node host only) |
invocation |
started |
requestId, event (JSON text), coldStart |
invocation |
completed |
requestId, durationMs, initMs (first invocation only), failed |
Throttles aren’t reported here. MiniStack publishes them as the AWS/Lambda Throttles metric in
CloudWatch.
const region = await createRegion({ lambda: { onOutput: ({ functionName, environment, text }) => console.log(`[${functionName} ${environment}] ${text}`), onEvent: (event) => { if (event.kind === 'invocation' && event.phase === 'completed' && event.failed) console.warn(`${event.functionName} failed`); }, },});