What's Changed
[1.3.3] — 2026-04-19
Added
- Lambda → CloudWatch Logs emission — every invocation now writes to
/aws/lambda/{FunctionName}(auto-created) on a per-invocation stream{yyyy}/{mm}/{dd}/[{qualifier}]{uuid}with AWS-shapedSTART RequestId:/ handler stdout+stderr /END RequestId:/REPORT RequestId: … Duration: N ms Billed Duration: N ms Memory Size: N MBlines. Unlocks Metric Filter / subscription filter / alarm testing chains that were previously impossible locally. Applies to every executor (Docker RIE, warm worker, provided-runtime, local subprocess). LAMBDA_STRICT=1env var — AWS-fidelity mode: every Lambda invocation runs in Docker via the AWS RIE image; in-process fallbacks are disabled. Missing Docker surfaces asRuntime.DockerUnavailableinstead of silently degrading to a subprocess. Opt-in; default behaviour keeps the no-Docker-required install path working.LAMBDA_WARM_TTL_SECONDSenv var — tunable idle TTL (default 300s) before the reaper thread evicts warm Docker containers from the pool.LAMBDA_ACCOUNT_CONCURRENCYenv var — account-level concurrent-invocation cap (default 0 = unbounded). Set to 1000 to match real AWS's default account limit and exerciseConcurrentInvocationLimitExceededthrottle paths.- Async retry + DLQ /
DestinationConfig.OnFailurerouting —Invoke(InvocationType=Event)and every internal event-source fan-out (currently: S3 notifications) now retry up toMaximumRetryAttempts(default 2) on failure and route the final failure to the configured DLQ (DeadLetterConfig.TargetArn) orOnFailuredestination (SQS / SNS / Lambda), with an AWS-shaped envelope (requestContext,requestPayload,responseContext,responsePayload). Sharedinvoke_async_with_retryhelper keeps direct async Invoke and event-source invocations on the same semantics. X-Amz-Function-Error: HandledvsUnhandleddistinction —_invoke_rienow reads RIE'sLambda-Runtime-Function-Error-Typeresponse header to classify raised-exception errors (Unhandled) separately from handler-returned error payloads (Handled), matching real AWS. The classification is surfaced in the Invoke response header.Retry-AfterHTTP header on 429 throttle responses —TooManyRequestsExceptionresponses now include both aretryAfterSecondsbody field and aRetry-AfterHTTP header, matching AWS.
Changed
- Lambda Docker executor — unified Zip/Image pool — restores the intent of @fzonneveld's #302: Zip and Image package types now share a single code path through the RIE warm pool (
_execute_function_imageis gone). The pool is a list-per-key ({account}:{fn}:{zip|image}:{sha|uri}) so concurrent invocations get separate containers, up toReservedConcurrentExecutions(unbounded by default, matching AWS). Thread-safe under_warm_pool_lock.reset()kills every pooled container across all accounts. A background reaper evicts idle containers past TTL. Regression fix from 1.2.20 — the post-merge commits on that release had split the paths back apart and reintroduced per-invocation cold starts for Image type. Originally contributed by @fzonneveld (#302).
Fixed
- Lambda Docker executor — Image type was cold-starting per invoke —
_execute_function_imagecreated a fresh container, invoked, then killed it. Image functions now share the same warm pool as Zip. - Lambda Docker executor — warm cache was single-container per key — concurrent invocations of the same function either serialised or created cold starts. The pool is now a list so up to
ReservedConcurrentExecutionsinvocations run in parallel from the pool. - Lambda Docker executor —
CodeSha256missing for Image package type — cache key was empty for Image-type, meaning different Image-type functions could collide. Cache key is now derived fromImageUrifor Image andCodeSha256for Zip, per-account.
Removed
ministack/core/lambda_wrapper.pyandministack/core/lambda_wrapper_node.js— dead code since the RIE-image migration. The AWS Lambda Runtime Interface Emulator provides the full runtime contract (handler loading, stdin/stdout, LambdaContext, boto3); the hand-rolled wrappers were never referenced after #302 landed. Removed.
Multi-tenancy correctness (8 CRITICAL cross-account leaks closed)
These services stored per-tenant data in plain dict / list, so List* / Describe* operations leaked rows across accounts. All now use AccountScopedDict. Cross-account isolation tests added to tests/test_multitenancy.py to lock in each fix.
- CloudWatch metrics + alarm history —
_metricsand_alarm_historywere global. Tenant A'sPutMetricDatawas visible to Tenant B'sListMetrics/GetMetricStatistics/DescribeAlarmHistory. - ElastiCache events —
_eventslist was global.DescribeEventsreturned all tenants' cache events. Also missing_tags.clear()fromreset(). - EventBridge —
_event_buses,_events_log,_partner_event_sourceswere all global. Tenants shared the same "default" event bus (with an ARN baked at module-load with whichever account first imported the module). The "default" bus is now seeded lazily per-tenant on first request so its ARN always matches the caller's account id. - Athena workgroups + data catalogs —
_workgroupsand_data_catalogswere global. Creating a workgroup namedmy-wgin Tenant A prevented Tenant B from creating one. The defaultprimaryworkgroup andAwsDataCatalogare now seeded lazily per-tenant. - SES sent emails —
_sent_emailslist was global.GetSendStatisticsaggregated across tenants. - API Gateway v1 —
_stages_v1,_deployments_v1,_authorizers_v1,_v1_tagswere all plain dicts. REST API stages / deployments / authorizers / tags leaked across tenants. New finding in this audit — APIGW v1 was not covered by earlier multi-tenancy reviews.
Lambda fixes
- Kinesis ESM
FilterCriteriafallback —NameError: name 'new_iter' is not defined— when all records in a Kinesis batch were filtered out, the poller tried to advance the shard position using an undefined local, crashing the poller thread silently. Now advances bypos + len(raw_records)(the full consumed batch) matching the success-path semantics.
AWS API parity
- Lambda
State/LastUpdateStatustransitions —CreateFunction,UpdateFunctionCode, andUpdateFunctionConfigurationnow returnState: "Pending"+LastUpdateStatus: "InProgress"initially, transitioning toActive/Successfulasynchronously. Terraform'sFunctionActiveandFunctionUpdatedwaiters now poll successfully instead of racing. Transition delay is tunable viaLAMBDA_STATE_TRANSITION_SECONDS(default0.5s). - Lambda
GetAccountSettings— new handler atGET /2016-08-19/account-settings, returnsAccountLimit(TotalCodeSize,CodeSizeUnzipped,CodeSizeZipped,ConcurrentExecutions,UnreservedConcurrentExecutions) andAccountUsage(TotalCodeSize,FunctionCount). Matches AWS response shape so Terraform data sources and CI tooling that probe the account-level limits work. - Lambda async retry exponential backoff —
invoke_async_with_retrynow sleeps between attempts (base1s, exponential, capped at30slocally — tunable viaLAMBDA_ASYNC_RETRY_BASE_SECONDS/LAMBDA_ASYNC_RETRY_MAX_SECONDS), and respectsMaximumEventAgeInSecondsso a retry that would push past the event age is skipped and routed to DLQ. AWS uses 1-minute base; scaled down locally to keep tests fast while preserving the shape. - Lambda
InvokeWithResponseStream— real vnd.amazon.eventstream framing — responses are now emitted as a validPayloadChunk+InvokeCompletesequence with correct prelude CRC + message CRC. boto3'sEventStreamparser decodes them natively. Handler errors flip to theInvokeErrorevent type with a JSON error body. - Lambda
GetFunction.Code.Location— pre-signed-style URL —GetFunctionnow returns a URL pointing at a new/_ministack/lambda-code/{fn}endpoint, dressed withX-Amz-Algorithm,X-Amz-Expires=600,X-Amz-Date,X-Amz-SignedHeaders,X-Amz-Signaturequery params so AWS SDKs andpip-style pull-and-extract scripts work against it unchanged. ForPackageType=Image,ResolvedImageUriis now populated (echo ofImageUri) alongsideImageUri. - Lambda
ListFunctionEventInvokeConfigs— new handler atGET /2019-09-25/functions/{name}/event-invoke-config/list. Returns the stored event-invoke config (one entry) or an empty list. - Lambda
GetFunctionCodeSigningConfig/PutFunctionCodeSigningConfig/DeleteFunctionCodeSigningConfig— real shape: GET returns{FunctionName, CodeSigningConfigArn}, PUT stores the ARN on the function, DELETE clears it. Was a stub returning empty fields. - Lambda REPORT log line — real
Max Memory Used— previously hardcoded0 MB. When the docker executor is used, peak RSS is now read fromcontainer.stats(); on non-docker executors it falls back toresource.getrusage(RUSAGE_CHILDREN).ru_maxrss(Linux/macOS normalised). Warm-worker subprocesses that never terminate still report0 MB— that matches "we don't have it" and avoids inventing a number. - Lambda ESM
FilterCriteriaapplied during polling — SQS / Kinesis / DynamoDB Streams pollers now evaluate each record against the ESM'sFilterCriteria.Filterspatterns and drop non-matching records before invoking the handler, matching AWS. Supports equality lists,prefix,suffix,anything-but,exists, andnumericcontent filters; SQS bodies are JSON-parsed for matching so patterns like{"body": {"orderType": ["Premium"]}}work as documented. - Lambda runtime image map —
java25,dotnet10— added to_RUNTIME_IMAGE_MAP, pointing atpublic.ecr.aws/lambda/java:25andpublic.ecr.aws/lambda/dotnet:10. Matches AWS's April 2026 runtime additions. - Lambda
DurableConfig/TenancyConfig/CapacityProviderConfig— new 2026-era optional config blocks are accepted onCreateFunction/UpdateFunctionConfiguration, stored, and echoed onGetFunction/GetFunctionConfiguration. Only emitted when set, matching AWS's response shape.