A behaviour, not a string
use Mayfly.Handler, implement handle/3, keep cold-start work in init/1. Compile-time checked; the Lambda handler setting is just the module name.
Mayfly is a lightweight, open-source Lambda custom runtime for Elixir.
Write a handler module, run mix release, attach the Erlang layer.
No Docker, no boilerplate, no silent failures.
Built and used in production by Karrer, an Elixir-on-AWS shop in Germany. MIT licensed.
Measured September 2026 on arm64, 512 MB, eu-central-1, with the Erlang layer. Cold starts vary with package size, memory and region.
use Mayfly.Handler, implement handle/3, keep cold-start work in init/1. Compile-time checked; the Lambda handler setting is just the module name.
Three release steps produce lambda.zip. Umbrellas, multiple releases and config/runtime.exs work as they always did.
Public Lambda layers ship the Erlang runtime. Your zip is a few MB of BEAM files and builds on macOS, Linux or Windows in seconds.
Exceptions, exits, bad return values, unencodable results and misconfigured handlers all reach Lambda as proper errors with Elixir stack traces.
Return any Enumerable. Function URL preludes, chunked encoding, mid-stream error trailers and backpressure are handled for you.
One poller process per concurrency slot. Invocations run in parallel on the BEAM, isolated by design, up to 32 GB of memory.
JSON logs with request and tenant ids, EMF metrics, :telemetry events incl. Lambda's own billed duration and memory via the Telemetry API, X-Ray propagation, and SIGTERM hooks before the environment is discarded.
mix lambda.invoke runs the real runtime against an emulated Runtime API; mix lambda.doctor catches setup mistakes. An Agent Skill and llms.txt teach coding agents the library.
mayfly_plug: a router or Phoenix endpoint is the handler. API Gateway, Function URL and ALB events become conns; send_chunked streams. Tested against a real Phoenix 1.8 endpoint.
mayfly_aws: idempotent handlers on DynamoDB, S3, SQS, SNS, SSM and Secrets Manager over a small SigV4 client. No dependencies, every call verified from Lambda.
mix lambda.new --iac sam|terraform|cdk [--http-api] generates a project with working infrastructure code, a Function URL or an API Gateway HTTP API, and the right layer ARN for every region; mix lambda.doctor checks the IaC against your toolchain. Or ship a container image with mix lambda.build --image. IaC guide · operations guide.
OTP 27, 28 and 29 on arm64 and x86_64, Managed Instances, streaming, images: each measured on real functions, with the numbers in the docs. CI deploys and invokes every published layer.
The event is a decoded map, the context carries request id, deadline and tenant, the result becomes JSON.
defmodule MyApp.Handler do
use Mayfly.Handler
@impl true
def init(_opts) do
# runs once per execution environment (cold start)
{:ok, %{table: System.fetch_env!("TABLE_NAME")}}
end
@impl true
def handle(event, %Mayfly.Context{} = ctx, state) do
{:ok, %{
message: "Hello, #{event["name"]}!",
request_id: ctx.request_id,
remaining_ms: Mayfly.Context.remaining_time_ms(ctx),
table: state.table
}}
end
end
Requires Elixir 1.18+ and OTP 27+. Pin your OTP to the layer's version, e.g. mise use erlang@27.3.4.18.
Shortcut: mix lambda.new hello --iac sam generates steps 1–3 plus deployable infrastructure code (IaC guide). By hand:
# mix.exs
def project do
[
app: :my_app,
deps: [{:mayfly, "~> 1.0.0-rc"}],
releases: [
lambda: [
steps: [&Mayfly.Release.prepare/1, :assemble,
&Mayfly.Release.bootstrap/1, &Mayfly.Release.zip/1],
mayfly: [handler: MyApp.Handler, layer: true]
]
]
]
end
mix lambda.invoke MyApp.Handler '{"name":"world"}'
mix lambda.doctor # release config, handler, OTP vs. layer
mix lambda.invoke runs the real runtime against an in-process Runtime API emulator, so errors, context and streaming behave exactly as in Lambda. Add --http to simulate a Function URL request.
MIX_ENV=prod mix release lambda
# * creating _build/prod/rel/lambda/bootstrap
# * creating _build/prod/rel/lambda/lambda.zip (1570 KiB)
Prefer to bundle the Erlang runtime instead of using a layer? Drop layer: true and run mix lambda.build --docker --arch arm64 (docker or finch).
aws lambda create-function --function-name hello \
--runtime provided.al2023 --architectures arm64 \
--handler MyApp.Handler \
--layers arn:aws:lambda:REGION:ACCOUNT:layer:mayfly-erlang-27-arm64:1 \
--zip-file fileb://_build/prod/rel/lambda/lambda.zip \
--role arn:aws:iam::ACCOUNT:role/lambda-role
The handler setting is the module name. SAM, CDK and Terraform templates are in the IaC guide.
BEAM files are architecture independent; only the Erlang runtime is not. Mayfly ships it as a Lambda layer, rebuilt weekly for the latest patch of each supported OTP major, for x86_64 and arm64, in 16 regions.
mayfly-erlang-27-3-4-18-arm64
One OTP patch release. Pin the full ARN in your infrastructure code.
mayfly-erlang-27-arm64
Newest version always tracks the latest 27.x patch.
layer/build.sh && layer/publish.sh
Publish to your account or AWS Organization in minutes.
The bootstrap verifies the layer's ERTS matches your build before starting the VM, so a version mismatch fails with a readable message.
A second, tiny layer, mayfly-shutdown-<arch>, makes Lambda send SIGTERM before discarding an environment so your shutdown hooks and log flush run.
Current ARNs: layer catalog (every OTP version, region and architecture, with a JSON resolver) ·
layer guide
Other ways to put Elixir on Lambda exist. This is where Mayfly differs.
| Approach | Build | Package | Streaming | Errors |
|---|---|---|---|---|
| Mayfly + Erlang layer | mix release on any OS | 1–5 MB | yes, incl. Function URL prelude | Elixir stack traces, typed |
| Mayfly, bundled ERTS | docker/finch, AL2023 | 25–50 MB | yes | same |
| Container image + Web Adapter | Docker, Phoenix app | 100+ MB image | yes | HTTP status codes |
| Hand-written bootstrap script | manual | varies | no | whatever you implement |
Implements the current Runtime API: invocation ids are echoed, tenant ids land in the context, error types follow Lambda's Category.Reason convention so the console classifies them correctly.
On Lambda Managed Instances a single environment serves AWS_LAMBDA_MAX_CONCURRENCY invocations in parallel, each in its own process. Verified on real hardware: eight concurrent invocations, one VM, 0.13 s wall time.
Cold start is BEAM boot: about half a second from the layer with a stripped release, single-digit milliseconds warm, ~85 MB resident.
Misconfigured handler? The function fails to start and Lambda reports Runtime.NoSuchHandler. Nothing is ever reported as a success that was not one.
Yes. AWS Lambda has no managed Elixir runtime, but its OS-only runtime (provided.al2023) runs any program that speaks the Lambda Runtime API. Mayfly implements that API in Elixir, so an ordinary Mix release runs as a Lambda function.
With Mayfly and the Erlang layer a stripped release cold-starts in roughly 500–600 ms on arm64; warm invocations take single-digit milliseconds and use about 85 MB of memory. Keep init/1 lean and the package small.
No. With the Mayfly Erlang layer the deployment zip contains only architecture-independent BEAM files and is produced by mix release on any OS. Docker (or finch) is only needed if you bundle ERTS yourself or have native NIF dependencies.
Yes. Return any Enumerable in a Mayfly.Response with stream/1; Mayfly uses chunked transfer encoding, adds the Function URL HTTP prelude and reports mid-stream errors via trailers. See the streaming guide.
Yes. Mayfly starts one poller process per AWS_LAMBDA_MAX_CONCURRENCY slot so a single execution environment handles that many invocations in parallel. Handler state from init/1 is shared read-only.
Elixir 1.18 or newer and Erlang/OTP 27 or newer. With the layer, your build toolchain's OTP must match the layer's OTP version exactly; mix lambda.doctor --layer ARN checks this and the bootstrap enforces it at start.
Yes for the HTTP/API part. The companion package mayfly_plug is a Plug.Conn.Adapter: use Mayfly.Plug.Handler, plug: {MyAppWeb.Endpoint, []} turns API Gateway, Function URL and ALB events into conns, runs your router or Phoenix endpoint and returns the proxy response, with send_chunked streaming through Lambda response streaming. No HTTP server in the release; a Phoenix 1.8 JSON API cold-starts in about 0.6–0.75 s with the Erlang layer. LiveView and channels need a WebSocket and are out of scope; use the Lambda Web Adapter with a container image for those. See the Phoenix guide.
Yes, with the mayfly-shutdown layer. Lambda only signals a runtime when an external extension is registered; this layer is a 260 KB static extension that does exactly that, and Mayfly turns the SIGTERM into Mayfly.Shutdown.register/1 hooks, a Logger.flush and a clean exit, within Lambda's 2 s window. Measured cold-start cost: none. See the observability guide.
mayfly_aws covers the calls most functions make (DynamoDB items, S3 objects and presigned URLs, SQS, SNS, SSM parameters, Secrets Manager) plus Mayfly.Idempotency for exactly-once handlers, in a few hundred lines on a SigV4 client with no dependencies. For everything else, Mayfly.AWS.Request.send/6 signs any request. ExAws works too if you prefer it.
Start with a Function URL: no extra service, no cost, IAM auth and response streaming. Add API Gateway (HTTP API) when you need custom domains, JWT authorizers, usage plans, WAF or several functions behind one host. mix lambda.new --iac … --http-api generates either; the handler code is the same because Mayfly.Events.HTTP decodes both payload shapes. Details and cost notes in the IaC and operations guides.
The handler is a module with use Mayfly.Handler instead of a Module.function string (the old form still works), the build is mix release, and Elixir 1.18+ is required. The migration guide lists every change.