All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.0.0-rc.1] - 2026-09-27
A redesign. See guides/migrating-from-0.x.md for the upgrade path.
Added
Mayfly.Handlerbehaviour withinit/1(cold-start state) andhandle/3;_HANDLERnow names a module. LegacyModule.functionhandlers still work.Mayfly.Release:prepare/1,bootstrap/1,zip/1release steps – the Lambda package is produced bymix release.mayfly: [layer: true]builds ERTS-less zips for the Mayfly Erlang layer.- ERTS layer tooling in
layer/(build.shwith an OTP version matrix,publish.shwith pinned + major-alias layer names, idempotent re-runs,--public/--orgsharing,latest-otp.sh,publisher-role.yml) and a weekly GitHub Actions workflow that publishes public layers for the latest patch of each supported OTP major to 16 regions on native runners. bootstrapfor layer builds verifies the layer is present and its ERTS version matches the release before starting the VM.mix lambda.invoke --http [--method M --path P]wraps the event like a Function URL / API Gateway v2 request.mix lambda.build --dockerworks withfinchwhen docker is absent and mounts externalpath:dependencies into the container.mix lambda.doctorchecks the release configuration, resolves the handler and runs itsinit/1, and compares the local OTP with a layer's (--layer ARN); without--layerit looks the matching public layer up in the catalog at elixir-aws-lambda.dev/layers for--arch/--region.- Dialyzer PLTs are kept in
_build/plts(apriv/pltslocation was shipped into consumer releases). - The Layers workflow publishes the ARN index to the website catalog
(
/layers/page and static JSON resolver). Mayfly.Response.stream/2acceptssend_timeout:to bound a stalled streaming write (default 30 s).bootstrapuses a single scheduler (+S 1:1) only on standard Lambda; on Managed Instances (AWS_LAMBDA_MAX_CONCURRENCY > 1) the VM sizes schedulers to the vCPUs. Managed Instances verified on real hardware.- Dialyzer runs in CI; a
Publishworkflow releases to Hex.pm onv*tags. skills/mayfly-elixir-lambda: an Agent Skill for coding agents, shipped in the Hex package.Mayfly.Events: typed decoders for API Gateway v1/v2 and Function URL requests (Mayfly.Events.HTTPwith JSON/base64-decoded bodies and response helpersjson/4,text/4,binary/5,redirect/3,respond/4), SQS, SNS (incl. SNS-in-SQS envelopes), S3 (URL-decoded keys), EventBridge, Kinesis and DynamoDB Streams (attribute values converted to terms);process_batch/2partial-batch helpers for SQS, Kinesis and DynamoDB;Mayfly.Events.decode/1dispatcher. Verified on Lambda with a Function URL, SQSReportBatchItemFailures, S3 notifications, EventBridge and DynamoDB Streams.mix lambda.new --http-api: API Gateway HTTP API ($defaultroute, JSON access logs, permission) in the SAM, Terraform and CDK templates; deployed and exercised on Lambda.mix lambda.doctorreads the IaC filesmix lambda.newgenerates and checks OTP major, selected layer ARN, architecture and runtime against the toolchain.- Guides: new
operations.md(alarms, Logs Insights queries, cost, Managed Instances sizing, runbook) andiac.md(split out of the deployment guide). Mayfly.Shutdown+ themayfly-shutdownexternal extension layer: with the layer attached Lambda sends SIGTERM before discarding the environment; Mayfly emits[:mayfly, :shutdown], runs registered hooks (1 s each), flushes Logger and halts. Verified on Lambda; no measurable cold-start cost.layer/build-shutdown-extension.sh,layer/publish-shutdown.sh, workflow job.- CI: OTP 29 and arm64 rows, container-image job invoking through the Runtime
Interface Emulator;
layers.ymlgained an opt-in post-publish smoke matrix (every OTP major × architecture deployed and invoked on Lambda). ROADMAP.mdwith the Durable Functions PR-FAQ.Mayfly.Extension(opt-in,MAYFLY_EXTENSION=1): internal Lambda extension that subscribes to the Telemetry API and emits[:mayfly, :platform, type]:telemetryevents with the record metrics;Mayfly.Metrics.attach_platform_metrics/2publishesDuration,BilledDuration,MaxMemoryUsed,MemorySize,InitDurationas EMF.Mayfly.LocalRuntimeemulates the Extensions and Telemetry APIs (push_telemetry/2,shutdown/2).Mayfly.HTTP.put/5. Verified on Lambda; no measurable cold-start cost. Lambda does not deliver SHUTDOWN to internal extensions, so there is no shutdown hook.mix lambda.new PATH [--iac sam|terraform|cdk] [--arch] [--otp] [--region]: generates a project (handler with HTTP branch, release config,.tool-versionspinned to the layer's OTP from the catalog, LocalRuntime test, README) plus a working SAM, Terraform or CDK definition.templates/with SAM, Terraform and a CDKMayflyFunctionconstruct whose layer ARN maps (16 regions × OTP 27/28/29 × arch) are rendered from the catalog bylayer/render-templates.sh, run weekly by the layers workflow. All three deployed, invoked and destroyed from generated projects.mix lambda.build --image [--tag] [--push ECR_URI]: container image for Lambda's image package type. Release built in the AL2023 build container, image based onpublic.ecr.aws/lambda/provided:al2023with the handler asCMD,lambda.image.Dockerfileoverride, ECR login/tag/push with digest, attestations disabled (Lambda rejects OCI indexes with them). Verified with a bcrypt C NIF on arm64: local RIE invoke and Lambda, cold start median 433 ms once cached.Mayfly.Metrics: CloudWatch Embedded Metric Format (emit/3,count/4,timing/4,build/3) andattach_invocation_metrics/2emittingDuration/Errors/ColdStartper invocation. Verified end to end: datapoints appear in CloudWatch under the namespace.- Companion package
mayfly_plug:Mayfly.Plug.Adapter(Plug.Conn.Adapterfor API Gateway v1/v2, Function URL and ALB events) anduse Mayfly.Plug.Handler, plug: …to run Plug routers and Phoenix endpoints as handlers, with response streaming forsend_chunked;guides/phoenix.mdwith measured cold starts. - Companion package
mayfly_awswithMayfly.Idempotency(DynamoDB-backed exactly-once execution), a minimal signed DynamoDB client andMayfly.AWS.SigV4;guides/idempotency.md. - Optional
:telemetrycalls go throughapply/3, so consumers without the dependency compile warning-free. mix lambda.invoke --event SOURCEwraps a payload in a realistic envelope (apigw-v2 apigw-v1 alb sqs sns s3 eventbridge kinesis dynamodb);--httpis now an alias for--event apigw-v2.Mayfly.Response: custom content types, response streaming with chunked transfer encoding, error trailers and the Function URL HTTP prelude (Mayfly.Response.http/2).Mayfly.Context:invocation_id,tenant_id,env;remaining_time_ms/1,logger_metadata/1.- Lambda Managed Instances:
AWS_LAMBDA_MAX_CONCURRENCYpollers run invocations concurrently in isolated processes. - Runtime API:
Lambda-Runtime-Invocation-Idecho, error-type header normalised toCategory.Reason,Lambda-Runtime-Function-Xray-Error-Cause,User-Agent. Mayfly.LogFormatter(JSON lines for advanced logging controls) installed automatically whenAWS_LAMBDA_LOG_FORMAT=JSON;AWS_LAMBDA_LOG_LEVEL/LOGLEVELhonoured.:telemetryevents (optional dependency).Mayfly.LocalRuntimeemulator andmix lambda.invoke.Mayfly.Boot.main/0explicit entry point;Mayfly.start_link/1public API.Runtime.InitError,Runtime.InvalidEventerror types.
Fixed (pre-release review, 2026-10-10)
- Each invocation runs in its own monitored process: a linked process dying or
Process.exit/2from handler code is reported to Lambda as anExiterror instead of killing the poller and timing the invocation out. - Error reports can no longer fail themselves: non-UTF-8
errorMessage/errorTypeare inspected, list bodies are validated as iodata, and a last-resort catch reportsRuntime.Unknown. Error types withRuntime./Function.prefixes are sanitised for the header; stack traces are trimmed at the handler boundary only. Mayfly.Response.stream/2'ssend_timeoutis honoured (it was stored but not passed on). Encode failures count as errors in telemetry.Mayfly.Bootonly sets the log level whenLOGLEVEL/AWS_LAMBDA_LOG_LEVELis set, and reports application start failures via/init/error.init/1is only called onMayfly.Handlermodules, not legacyModule.functionhandlers.rel/overlayskeeps working alongside Mayfly's release templates;--dockerbuilds pass the toolchain's OTP/Elixir as build args so layer-mode releases match the public layer; container images no longer includelambda.zip.- ALB
source_ipis the lastX-Forwarded-Forhop; v2 responses join list-valued headers. Mayfly.Extensionretries/event/nextwith backoff and halts after repeated failures instead of leaving invocations to time out; the listener binds to loopback; the accept loop survives transient errors.Mayfly.Shutdownbounds all hooks by a 1.2 s deadline;register/1is a no-op outside Lambda.- Hex package excludes
layer/distandpriv/plts, ships only the user skill, and the publish workflow refuses packages over 2 MB.
Considered and not shipped
- An Elixir layer (
mayfly-elixir-<vsn>-otp-<major>, zip 0.16 MB instead of 1.6 MB). Measured on Lambda: cold start median 511 ms vs 543 ms with the ERTS layer alone (21 forced cold starts each, arm64, 512 MB), p90 541 vs 709 ms, deploy upload about one second faster. Below the 50 ms bar for a second version axis; see "Why there is no Elixir layer" in the layers guide.
Changed
- Requires Elixir 1.18+; uses the built-in
JSONmodule. Jason removed. Mayfly.HTTP(:gen_tcp) replaces:httpc;:inets/:sslno longer required at boot.mix lambda.buildonly builds the configured release (optionally in Docker) and copieslambda.zip; noMIX_ENV=lambda, defaults toprod.errorTypehas noElixir.prefix;stackTraceis a list;{:error, "s"}isHandlerError; non-{:ok,_}/{:error,_}returns areRuntime.InvalidResponse.Runtime.HandlerNotFoundrenamed toRuntime.NoSuchHandler(AWS-recommended).- Modules renamed:
Mayfly.Error→Mayfly.ErrorPayload,Mayfly.Runtime→Mayfly.RuntimeAPI,Mayfly.Loop→Mayfly.Poller. bootstrapusesbin/<release> eval "Mayfly.Boot.main()", setsRELEASE_TMP=/tmp,RELEASE_DISTRIBUTION=none;vm.argstuned for Lambda.lambda.Dockerfilepins OTP 27.3.4 / Elixir 1.18.4, is multi-target and architecture-neutral.
Removed
- Application callback (
mod:) – nothing starts implicitly. config :mayfly, start_loop,Mayfly.Handler.default_handler,Mayfly.hello,Mayfly.Helpers, the committedbootstrapfile.
Fixed (relative to 0.1.0)
- Misconfigured handlers were reported as successful invocations.
- Unencodable results left invocations hanging until timeout.
exit/throwin handlers crashed the event loop.KeyErrorand friends producederrorMessage: null.- Poll failures spun without backoff; HTTP status codes were ignored; bodies were charlists.
zipbinary dependency, paths with spaces,--outdirignored for Docker.
[0.1.0] - 2025-11-21
Added
- Initial release of Mayfly - AWS Lambda Custom Runtime for Elixir
- Core Lambda Runtime API integration with long-polling support
- Handler resolution and execution with proper error handling
- Comprehensive error formatting with stacktraces
- Mix task
lambda.buildfor creating deployment packages - Docker build support for cross-platform compatibility
- ZIP archive creation for Lambda deployment
- Bootstrap script generation
- Security: Using
String.to_existing_atom/1to prevent atom exhaustion - Timeout configuration: 5s for responses, infinite for long-polling
- Graceful error handling for missing request IDs and malformed JSON
- OTP supervision tree for fault tolerance
- Comprehensive documentation and README with examples
Features
- Zero boilerplate Lambda function development
- Native Elixir experience with standard
{:ok, result}/{:error, reason}patterns - Support for API Gateway, S3, EventBridge and other AWS event sources
- Proper Elixir stacktraces in Lambda error responses
- Flexible handler configuration via
_HANDLERenvironment variable - Build tooling with
--zip,--docker, and--outdiroptions
Technical Details
- Elixir ~> 1.15 support
- Single dependency: Jason for JSON encoding/decoding
- Uses Erlang's
:httpcfor Lambda Runtime API communication - GenServer-based event loop for continuous invocation processing