# AWS Lambda

> Deploy a FrontMCP server to AWS Lambda with frontmcp build --target lambda: the handler it writes, what to deploy, NODE_ENV, the events it accepts, and storing sessions in Redis.

Source: https://frontmcp.dev/reference/deployment/aws-lambda

`frontmcp build --target lambda` bundles your server, FrontMCP and all, into one file that exports a Lambda `handler`. The handler turns API Gateway and function URL events into requests for FrontMCP's Express app, through `@codegenie/serverless-express`, which you install and the build puts in the bundle. Like every serverless build, it keeps nothing between invocations unless you give it Redis, and answers `/healthz` but not `/readyz`. It's in production mode when `NODE_ENV=production`, which Lambda doesn't set: set it in the function's environment.

```bash
npm install @codegenie/serverless-express
frontmcp build --target lambda    # dist/lambda/handler.cjs, exports handler
```

---

## Reference

### What the build writes

```text
dist/lambda/
  main.js, help-desk.app.js, …   your compiled code
  serverless-setup.js            sets FRONTMCP_SERVERLESS=1, FRONTMCP_DEPLOYMENT_MODE=serverless and FRONTMCP_HTTP_ENTRY_PATH
  index.js                       the entry: loads the setup and your server, exports handler
  handler.cjs                    index.js and everything it imports, in one file (about 10 MB)
  queue.widget.tsx, …            your tools' *.widget.tsx and *.widget.jsx files, copied beside handler.cjs
```

```js dist/lambda/index.js
require('./serverless-setup.js');
require('./main.js');
const { getServerlessHandlerAsync } = require('@frontmcp/sdk');
const serverlessExpressMod = require('@codegenie/serverless-express');
const serverlessExpress = serverlessExpressMod.default || serverlessExpressMod;

let serverlessExpressInstance = null;

async function setup() {
  const app = await getServerlessHandlerAsync();
  serverlessExpressInstance = serverlessExpress({ app });
}

exports.handler = async (event, context) => {
  if (!serverlessExpressInstance) {
    await setup();
  }
  return serverlessExpressInstance(event, context);
};
```

The first invocation of a new instance builds the server; later ones reuse it.

#### What to deploy

`handler.cjs` contains FrontMCP, your dependencies and `@codegenie/serverless-express`, so it needs no `node_modules`: copied alone into an empty folder, it answered `/healthz`, a `tools/call` and an `initialize`, and reached Redis with `redis` set. Deploy `dist/lambda/` as it is, or `handler.cjs` on its own. The function's handler is `handler.handler`, on the `nodejs24.x` runtime that `frontmcp create` picks; the `ci/template.yaml` it writes has that handler, and `CodeUri: ../dist/lambda/`. A tool with a widget file reads it beside `handler.cjs` when it's called, so deploy the `*.widget.tsx` files that the build put next to it in `dist/lambda/` too ([widget sources](https://frontmcp.dev/reference/deployment/production-build#widget-sources)).

The build needs `@codegenie/serverless-express` installed in the project, which `frontmcp create --target lambda` adds, and stops without it: [`missing required peer dependency`](#--target-lambda-missing-required-peer-dependency-codegenieserverless-express). Optional packages that aren't installed, like `@upstash/redis`, stay out of the bundle, and the build finishes without a warning. Changed in 1.9.3: in 1.9.2 the build stopped on `@upstash/redis` ([below](#module-not-found-cant-resolve-upstashredis)). See [what's in the bundle](https://frontmcp.dev/reference/deployment/production-build#whats-in-the-bundle).

Changed in 1.9: before, `handler.cjs` left `@codegenie/serverless-express` out, so a function deployed from `dist/lambda/` failed on load with `Cannot find module '@codegenie/serverless-express'` unless you shipped the package beside it.

### What the function serves

| Request | Answer |
| --- | --- |
| MCP at `http.entryPath`, or `/` | As FrontMCP's Node server answers it. A response that streams, like `initialize` from a client before MCP 2026-07-28, arrives whole, as one body of `text/event-stream` events. |
| `GET /healthz`, `GET /health` | `200`, with `"deployment": "serverless"` and `"env": "production"`. |
| `GET /readyz` | `404`: readiness checks are off in serverless mode. |

The handler took both API Gateway event formats: HTTP APIs and function URLs (payload version 2.0), and REST APIs (1.0).

Like the [Vercel](https://frontmcp.dev/reference/deployment/vercel#production-when-node_env-says-so) bundle, it reads `NODE_ENV` when it starts, and Lambda doesn't set it: without `NODE_ENV=production` in the function's environment, `/healthz` says `"env": "development"`, and the function makes up the secrets it should require and shows callers the messages of plain errors. In production, give it `MCP_SESSION_SECRET` if clients on protocol versions before 2026-07-28 use it: without it their `initialize` answers `500` with `{"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED",…}`, while 2026-07-28 calls and `/healthz` answer `200` (changed in 1.8.5; before, every anonymous or static-key call got the `500`). `local` and `remote` auth need `JWT_SECRET` too.

`transport.http.path` in `frontmcp.config` reaches the function: a project made with `frontmcp create` serves `/mcp`, and `http.entryPath` in `@FrontMcp` wins over it. A request to `/` is a `404`. Route the API's `/{proxy+}` to the function either way. Changed in 1.8.7: the handler used to be always in production, whatever `NODE_ENV` said, and served `/` unless `http.entryPath` was set.

### Storage

Lambda runs many instances, and each keeps its own memory. Clients on MCP 2026-07-28 need nothing more. Sessions of older clients, background tasks and pending questions need a Redis every instance can reach, like ElastiCache in the function's VPC:

```ts main.ts
@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  redis: { url: process.env.REDIS_URL! }, // like rediss://default:…@desk-cache.example.com:6379
})
export default class Server {}
```

[Redis](https://frontmcp.dev/reference/deployment/redis) has every option. The decorator runs when the instance loads the bundle, so `process.env` in it reads the function's environment variables.

#### Caveats

- `frontmcp create --target lambda` writes `ci/template.yaml` with `CodeUri: ../dist/lambda/` and `Handler: handler.handler`, which find the handler since 1.8.7 (before, `CodeUri: ../dist/` and `Handler: main.handler`, a file that doesn't exist), and run since 1.9. Since 1.9.2 it also sets `NODE_ENV: production` and `MCP_SESSION_SECRET` ([below](#deploying-with-sam)); a template written by an older `create` sets neither.
- The startup log warns `SSE transport is not supported in serverless deployments`, and without Redis, `No distributed storage backend detected in production. Using in-memory storage.`
- This site couldn't deploy to AWS. With `frontmcp` 1.9.3, the build and the handler were checked by calling `handler.cjs`, alone in an empty folder, from Node with HTTP API (2.0) events: `/healthz`, `/readyz`, a `tools/call` at `/mcp` and an `initialize`, with and without `NODE_ENV=production`, and with `redis` pointed at Valkey 8. A call at `/` was last checked with 1.9.1, REST API (1.0) events with 1.8.4, SAM wasn't run, and that Lambda leaves `NODE_ENV` unset wasn't checked on AWS.

---

## Usage

### Building the handler

```bash
npm install @codegenie/serverless-express   # frontmcp create --target lambda already did
npx frontmcp build --target lambda
```

`dist/lambda/` is what Lambda runs. Only `handler.cjs`, and the widget files beside it, are used: the compiled files next to them are already in the bundle.

### Deploying with SAM

```yaml ci/template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: help-desk - FrontMCP Lambda Function

Parameters:
  McpSessionSecret:
    Type: String
    NoEcho: true
    Description: MCP_SESSION_SECRET for session-ID encryption, required in production. Generate it once (openssl rand -hex 32) and pass the same value on every deploy; a new value ends every open session

Globals:
  Function:
    Timeout: 30
    Runtime: nodejs24.x
    MemorySize: 256
    Environment:
      Variables:
        NODE_ENV: production
        MCP_SESSION_SECRET: !Ref McpSessionSecret

Resources:
  FrontMCPFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ../dist/lambda/
      Handler: handler.handler
      Events:
        ApiEvent:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY

Outputs:
  ApiEndpoint:
    Description: API Gateway endpoint URL
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
```

This is the template `frontmcp create --target lambda` writes. `NODE_ENV: production` puts the function in production mode, and `McpSessionSecret` is a required parameter, marked `NoEcho` so CloudFormation doesn't show its value: `sam deploy` needs a value for it, which belongs in your secret store, not in a file in the repository. Generate it once and pass the same value on every deploy: an id made under another secret gets `404` `invalid session id`, so a new value ends every open session. The project's `deploy` script runs `cd ci && sam build && sam deploy`. SAM wasn't run for this page.

### Calling the handler locally

A test can call `handler` with an event, the way Lambda does, before anything is deployed:

```js invoke.cjs
const { handler } = require("./dist/lambda/handler.cjs");

const event = {
  version: "2.0",
  routeKey: "$default",
  rawPath: "/healthz",
  rawQueryString: "",
  headers: { host: "abc123.execute-api.eu-west-1.amazonaws.com" },
  requestContext: { http: { method: "GET", path: "/healthz", protocol: "HTTP/1.1", sourceIp: "203.0.113.7", userAgent: "test" }, requestId: "1", routeKey: "$default", stage: "$default" },
  isBase64Encoded: false,
};

handler(event, { awsRequestId: "1", getRemainingTimeInMillis: () => 30000 }).then((response) => {
  console.log(response.statusCode, response.body);
});
```

```bash
NODE_ENV=production node invoke.cjs
```

```text
200 {"status":"ok","server":{"name":"help-desk","version":"1.0.0"},"runtime":{"platform":"darwin","runtime":"node","deployment":"serverless","env":"production"},"uptime":0.061}
```

A `POST` event with an MCP body and the 2026-07-28 headers ([request headers](https://frontmcp.dev/reference/sdk/create-fetch-handler#request-headers)) returns the tool's result the same way.

---

## Troubleshooting

### `[--target lambda] missing required peer dependency @codegenie/serverless-express`

The build checks that the package is installed in the project. `npm install @codegenie/serverless-express`.

### `Cannot find module '@codegenie/serverless-express'`

The handler was built by FrontMCP 1.8.7 or earlier, which left the package out of the bundle. Build it again with 1.9.3, or deploy `node_modules/@codegenie/serverless-express` next to it. See [what to deploy](#what-to-deploy).

### The function can't find its handler

The function's handler names a file that isn't in the package. With the folder above it's `handler.handler`. A template that an older `frontmcp create` wrote uses `main.handler` in `dist/`, which the build doesn't produce.

### `initialize` answers `500`, with `"error":"server_misconfigured"` and `SESSION_SECRET_REQUIRED`

`MCP_SESSION_SECRET` isn't set, the function is in production (`NODE_ENV=production`), and a client on a protocol version before 2026-07-28 tried to open a session. `/healthz` and 2026-07-28 calls still answer `200`, so a health check doesn't catch it.

### `/healthz` says `"env": "development"`

The function's environment has no `NODE_ENV=production`, and Lambda doesn't set it. Add it, as in [the template](#deploying-with-sam), which `frontmcp create` writes with it since 1.9.2.

### `Module not found: Can't resolve '@upstash/redis'`

A `lambda` build of 1.9.2, which stopped on this optional package when it wasn't installed. Upgrade `frontmcp` to 1.9.3, which leaves it out, or `npm install @upstash/redis`, which bundles it into `handler.cjs`. See [what's in the bundle](https://frontmcp.dev/reference/deployment/production-build#whats-in-the-bundle).

### `/readyz` answers `404`

Readiness checks are off in serverless mode. Use `/healthz`.

### Older clients get `404` `session not initialized`

Their next request reached another instance, or a new one. Give the server a [Redis](#storage) every instance can reach. `404` `invalid session id` instead means the id was made under another `MCP_SESSION_SECRET`: every copy of the function needs the same one.
