Skip to content

Run Elixir on AWS Lambda

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 , an Elixir-on-AWS shop in Germany. MIT licensed.

Cold start
~0.5 s
Warm invoke
2 ms
Package size
1.5 MB
Runtime deps
0

Measured September 2026 on arm64, 512 MB, eu-central-1, with the Erlang layer. Cold starts vary with package size, memory and region.

Everything Lambda offers, in idiomatic Elixir

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.

mix release is the build

Three release steps produce lambda.zip. Umbrellas, multiple releases and config/runtime.exs work as they always did.

Erlang layer, no Docker

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.

Honest errors

Exceptions, exits, bad return values, unencodable results and misconfigured handlers all reach Lambda as proper errors with Elixir stack traces.

Response streaming

Return any Enumerable. Function URL preludes, chunked encoding, mid-stream error trailers and backpressure are handled for you.

Managed Instances

One poller process per concurrency slot. Invocations run in parallel on the BEAM, isolated by design, up to 32 GB of memory.

Observable

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.

Local first, agent friendly

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.

Plug and Phoenix

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.

AWS without an SDK

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.

Deploy your way

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.

Verified on Lambda

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.

An Elixir Lambda function is a module

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

From zero to Lambda in four steps

Requires Elixir 1.18+ and OTP 27+. Pin your OTP to the layer's version, e.g. mise use erlang@27.3.4.18.

  1. Add the dependency and a release

    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
  2. Write the handler and try it locally

    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.

  3. Build

    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).

  4. Deploy

    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.

Erlang runtime layers for AWS Lambda

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.

Pinned

mayfly-erlang-27-3-4-18-arm64

One OTP patch release. Pin the full ARN in your infrastructure code.

Alias

mayfly-erlang-27-arm64

Newest version always tracks the latest 27.x patch.

Your own

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

How Mayfly compares

Other ways to put Elixir on Lambda exist. This is where Mayfly differs.

ApproachBuildPackageStreamingErrors
Mayfly + Erlang layermix release on any OS1–5 MByes, incl. Function URL preludeElixir stack traces, typed
Mayfly, bundled ERTSdocker/finch, AL202325–50 MByessame
Container image + Web AdapterDocker, Phoenix app100+ MB imageyesHTTP status codes
Hand-written bootstrap scriptmanualvariesnowhatever you implement

Built for how Lambda works today

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.

Frequently asked questions

Can you run Elixir on AWS Lambda?

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.

How fast does Elixir cold start on Lambda?

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.

Do I need Docker to build an Elixir Lambda function?

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.

Does Mayfly support Lambda response streaming?

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.

Does Mayfly work with Lambda Managed Instances?

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.

Which Elixir and OTP versions does Mayfly require?

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.

Can I run a Phoenix app on Lambda with Mayfly?

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.

Can my function clean up before Lambda shuts the environment down?

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.

How do I call S3, SQS or DynamoDB without ExAws?

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.

Function URL or API Gateway?

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.

I'm on Mayfly 0.x. What changes?

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.