Skip to main content

Remix 3 on Lambda

· 10 min read
Ivan Barlog
AWS Solutions Architect @ BeeSolve

I was super excited to hear the news about Remix 3 stable being released last week. Similarly to SvelteKit 3 I wanted to run it in AWS Lambda. For SvelteKit I've built kit-on-lambda and I was curious if something similar could be done for Remix as well.

What's all the fuss about Remix 3​

I loved both previous versions of Remix and when I heard last year that they are going to divorce React I was intrigued. I followed Remix from day one and I love their passion about Web APIs and standards. I strongly recommend seeing both Remix Jam 2025 and Remix Jam 2026 - in the former there is an introduction of new concepts which Remix 3 is going to be built upon and in the latter the actual Remix 3 introduction.

The Remix team came up with 6 principles which they followed whilst developing Remix 3. Here is a recap with my comment on them:

  1. Model-First Development. I believe this makes perfect sense in this day and age and since I am adopting LLM assisted development myself for the last few months I am happy to see Remix going in this direction. They provide their own skill which is bundled with demo application plus all of their packages contain documentation right inside node_modules for easier agent navigation.
  2. Build on Web APIs. I absolutely love this and this is one of the reasons I was interested in Remix from start. I believe we should use the platform as much as possible.
  3. Religiously Runtime. This is somewhat controversial and I am going to have some thoughts on this later in the article. Basically it means "no build step" not just for dev but also prod environments. This is again simplification of the web development.
  4. Avoid Dependencies. Yes. Please! Love this absolutely. Does this mean that they might have written a few things from scratch? They might have been inspired by other projects? Maybe but that's OK since your agent will roll their own implementation most of the time anyway and with this code at least you can be sure that someone who really understands the topic has reviewed it. Also this doesn't mean you cannot get your own dependencies which has been clearly stated by the Remix team on several occasions.
  5. Demand Composition. Again, yes! The composition wins every time. They ditched React but they still use JSX and composition of components. On top of that you can now compose "mixins" meaning you can compose and reuse behaviours and styles. This is great win.
  6. Distribute Cohesively. Having single "remix" package is great. Unfortunately the "religiously runtime" again fights the serverless architectures which we are going to discuss next.

A lot of great stuff has been built thanks to these principles. The Remix team stayed true to them rigorously. This allowed them to build great simplifications and avoid complex abstractions. There are trade-offs as always but I would say that this framework is by far the closest thing to the Web APIs and standards I've seen in years. And I love all of it.

Let's run Remix 3 in Lambda​

Remix 3 is built around Fetch API which is great. I already have built a utility which allows you to run anything built around Fetch API inside Lambda with Function URL or behind a REST or HTTP API Gateway. So this should be sufficient:

// lambda.ts
import { asHttpV2Handler } from "@beesolve/lambda-fetch-api";

import { router } from "./app/router.ts";

export const handler = asHttpV2Handler(router.fetch);

After this we should be able to simply wire things up with CDK and deploy it. We need to deploy full node_modules as there is no build step (remember Religiously Runtime) which means we need to ship additional 87 MB of non application code. This 87 MB consists of Remix framework itself and a lot of Node.js modules which are responsible for parsing, transformation and minimization of TypeScript, JSX and CSS. 17 MB of this are just oxc* and lightningcss binaries.

du -sh node_modules/**/*.node
2.3M node_modules/@oxc-minify/binding-darwin-arm64/minify.darwin-arm64.node
2.1M node_modules/@oxc-parser/binding-darwin-arm64/parser.darwin-arm64.node
1.5M node_modules/@oxc-resolver/binding-darwin-arm64/resolver.darwin-arm64.node
3.7M node_modules/@oxc-transform/binding-darwin-arm64/transform.darwin-arm64.node
8.1M node_modules/lightningcss-darwin-arm64/lightningcss.darwin-arm64.node

I don't like to bundle my Lambda code in Docker image as it adds complexity to my environment and CDK does not support Apple Containers which I am using instead of the Docker but I believe it will be much less pain to do it this way because of various constraints as for instance size of packaged Lambda code. Since Remix is running TypeScript and JSX on the fly it needs custom loader for Node.js - usually you just need to add NODE_OPTIONS=--import remix/node-tsx to your env variables but since AWS Lambda Runtime Interface Client (RIC) cannot run .ts on itself we need to provide .js, .cjs or .mjs and we need to wire up the loader ourselves. Fortunately this is where LLM can help a lot so instead of loading lambda.ts we will provide lambda.mjs:

// lambda.mjs
import { loadModule } from "remix/node-tsx/load-module";

const mod = await loadModule("./lambda.ts", import.meta.url);

export const handler = mod.handler;

Now inside the Dockerfile we just install dev dependencies, copy the code and provide handler through CMD ["lambda.handler"]. And finally we can wire it up in CDK:

// cdk.ts
const handler = new DockerImageFunction(stack, "Handler", {
code: DockerImageCode.fromImageAsset(".", {
platform: Platform.LINUX_ARM64,
}),
architecture: Architecture.ARM_64,
memorySize: 1024,
});

handler.addFunctionUrl({ authType: FunctionUrlAuthType.NONE });

The whole sample is available on GitHub at ivanbarlog/remix-on-lambda. It also contains the CDK code which puts CloudFront in front of the Lambda Function URL and caches /assets* for better performance.

"Religiously runtime" meets serverless​

Probably the single most impactful thing which helps you to mitigate cold starts in serverless world is size of the actual code you are going to run. This is why this article was originally called "Remix 3: not the serverless framework". When I realized that you are going to ship 160+ MB container just to serve simple web page I thought this won't really work with AWS Lambda. The problem is that over the years I've forgotten about what actually Lambda can do since I've been optimizing my stack probably too much.

Currently when I build web applications through kit-on-lambda the production bundle is less than 1 MB which contains whole SvelteKit 3 framework, valibot, Graffiti UI and AWS SDKs for S3, SQS and DynamoDB. Of course the 1 MB is achievable only thanks to build with esbuild and tree-shaking of libraries.

So 160+ MB vs ~1 MB in my mind sounded like "not worth even trying" but then I realized I might be wrong so I've asked my agent to build sample TODO app wired to the DynamoDB and after packaging it within Docker container I deployed it to AWS Lambda. Below are the results which are not super empirical as there is no throttling + this has been run on my machine. This is just so you have an idea of how Remix actually performs + how big of a tax would you need to pay in cold start time for those 160+ MB of runtime.

Simplest architecture​

Below is the diagram of the architecture and also some timelines which I've "measured". This is Remix 3 in Docker image served through AWS Lambda Function URL. I've tested cold start, warm start with hard refresh, warm start with soft refresh and warm start with browser cache enabled. The cold start here accounts for 2-3 seconds in general which is not that bad but I am not sure if you want to let your users wait that much - but again it is just from time to time depending on your site traffic. My conclusion is actually "not that bad".

Cold start - requests
Cold start - timeline
Warmed with hard refresh - requests
Warmed with hard refresh - timeline
Warmed with soft refresh - requests
Warmed with soft refresh - timeline
Warmed with cache - requests
Warmed with cache - timeline

Using CloudFront for caching​

I've done the same "test and measurement" for the AWS Lambda Function URL behind CloudFront instance (see the diagram below) - this time the assets don't need to be regenerated every time for each new user but they are built once and cached to CloudFront for up to a year meaning you get fewer hits to your Lambda function and your users get lower latencies in general. The tests are the same as above. The results are similar for this particular type of tests but in general I would still recommend fronting the AWS Lambda Function URL with the CloudFront for better performance and lower bill in general.

Cold start behind CloudFront - requests
Cold start behind CloudFront - timeline
Warmed with hard refresh behind CloudFront - requests
Warmed with hard refresh behind CloudFront - timeline
Warmed with soft refresh behind CloudFront - requests
Warmed with soft refresh behind CloudFront - timeline
Warmed with cache behind CloudFront - requests
Warmed with cache behind CloudFront - timeline

Conclusion​

I was wrong. Not about Remix being great but about Remix not being able to run in serverless environment.

If you want to build quick Web API based applications and you can absorb 2-3 seconds of cold starts from time to time then Remix on Lambda is great choice for you.

This was just a demo and I just wanted to quickly check if Remix is even viable option for me these days but I believe I might come back to Remix soon.

Running Remix without build step might not be the most performant or optimal way but it is doable. If I spent more time (and probably tokens) I believe I could introduce build step which would enable me to run the Remix 3 natively in Lambda Node.js runtime and I also believe it would match the size of code of SvelteKit 3. For now I guess I'll wait a bit and see if anyone else will have the same ideas.