ITADN

Arcjet exceeding Vercel middleware edge function limit on Vercel's Hobby plan

#5967OpenMarkBekooy 创建于 2026-04-01
M
MarkBekooycommented
# Introduction to the problem In August 2025 I created a side project app that used Arcjet version `1.0.0-beta.9` which deployed perfectly fine to Vercel. However, this week, I updated the Arcjet dependency in this project to the latest version (`1.3.1`) and noticed that I could not deploy my app to Vercel anymore, because I'm on their Hobby plan, which threw the error: `Error: The Edge Function "src/middleware" size is 1.04 MB and your plan size limit is 1 MB.` # Research After 2 hours of debugging, I concluded that Arcjet is indeed the bottleneck and wanted to inform you of this problem. Here is the screenshot of my Vercel deployment log with corresponding commits: <img width="1122" height="658" alt="Image" src="https://github.com/user-attachments/assets/05ff2d9b-5886-4b0a-884d-941cd72ea368" /> Arcjet version `1.0.0-beta.10` still worked, but from version `1.0.0-beta.11` and higher, the deployments started to fail. I first thought that it might be my Sentry configuration, next-intl configuration or using Next's Turbopack, but even after removing all of these, the deployments still would not succeed. # The problem (as diagnosed by GPT-5.4 and confirmed by me) ## Why It’s Happening Vercel’s Hobby limit is **1 MB for the compressed Edge bundle**, not just the source file size. That bundle includes all code and assets transitively imported by `src/middleware.ts`, including WASM files. Your middleware imports Arcjet at the top level: ```1:6:src/middleware.ts import type { NextRequest } from "next/server"; import { detectBot } from "@arcjet/next"; import createMiddleware from "next-intl/middleware"; import { NextResponse } from "next/server"; import arcjet from "@/libs/Arcjet"; import { routing } from "./libs/I18nRouting"; ``` And even though execution is conditional, bundling is not: ```24:38:src/middleware.ts export default async function middleware( request: NextRequest, // event: NextFetchEvent, ) { // Verify the request with Arcjet // Use `process.env` instead of Env to reduce bundle size in middleware if (process.env.ARCJET_KEY) { const decision = await aj.protect(request); if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } } return handleI18nRouting(request); } ``` The built middleware manifest shows that this middleware bundle contains **three WASM assets**: ```17:30:.next/server/middleware-manifest.json "wasm": [ { "name": "wasm_e5b98722e1c3c3a66f25857f324ec746898ca8bc", "filePath": "server/edge-chunks/wasm_e5b98722e1c3c3a66f25857f324ec746898ca8bc.wasm" }, { "name": "wasm_9157ebd62adbcf6236a6113119c93e03a850eaca", "filePath": "server/edge-chunks/wasm_9157ebd62adbcf6236a6113119c93e03a850eaca.wasm" }, { "name": "wasm_09d302e9dd6fc381c4e82b71f1102bd7a58958cf", "filePath": "server/edge-chunks/wasm_09d302e9dd6fc381c4e82b71f1102bd7a58958cf.wasm" } ], ``` I measured the built output locally: - middleware JS gzip: about `98,866` bytes - main WASM gzip: about `921,156` bytes - total gzip: about `1,021,195` bytes That matches the Vercel error almost exactly. So the real cause is not your i18n middleware, it’s the **Arcjet Edge bundle**, especially the WASM it pulls in. ## Why `next build` Looks Fine Local `next build` reports: - `ƒ Middleware 100 kB` That number is misleading for this case. It reflects the middleware code bundle size that Next prints, but **Vercel enforces the final compressed Edge function bundle**, which also includes traced assets like WASM. ## How To Get Under 1 MB The practical fix is to **remove Arcjet from `src/middleware.ts`**. What will help: - Keep middleware limited to `next-intl` locale routing. - Move Arcjet protection into specific `route.ts` handlers, server actions, or other Node-side endpoints. - Protect only the endpoints that need it, like your `/api/beer-search` route. What will **not** help much: - Keeping the `if (process.env.ARCJET_KEY)` check. That affects runtime execution, not bundling. - Tightening the `matcher`. That reduces where middleware runs, not what gets bundled. - Small import cleanups. You’re over the limit mainly because of a ~`921 kB` gzipped WASM blob, so this needs an architectural move, not micro-optimization. # Good to know I created my application from a [popular Next template](https://github.com/ixartz/Next-js-Boilerplate) which has their middleware (nowadays called proxy.ts like [this](https://github.com/ixartz/Next-js-Boilerplate/blob/main/src/proxy.ts). (I have the exact same setup in my app, except I removed Clerk) # My files (useful context) My Next.js 15.5.14 middleware.ts file is as follows: ```javascript import type { NextRequest } from "next/server"; import { detectBot } from "@arcjet/next"; import createMiddleware from "next-intl/middleware"; import { NextResponse } from "next/server"; import arcjet from "@/libs/Arcjet"; import { routing } from "./libs/I18nRouting"; const handleI18nRouting = createMiddleware(routing); // Improve security with Arcjet const aj = arcjet.withRule( detectBot({ mode: "LIVE", // Block all bots except the following allow: [ // See https://docs.arcjet.com/bot-protection/identifying-bots "CATEGORY:SEARCH_ENGINE", // Allow search engines "CATEGORY:PREVIEW", // Allow preview links to show OG images "CATEGORY:MONITOR", // Allow uptime monitoring services ], }), ); export default async function middleware( request: NextRequest, // event: NextFetchEvent, ) { // Verify the request with Arcjet // Use `process.env` instead of Env to reduce bundle size in middleware if (process.env.ARCJET_KEY) { const decision = await aj.protect(request); if (decision.isDenied()) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } } return handleI18nRouting(request); } export const config = { // Match all pathnames except for // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel` // - … the ones containing a dot (e.g. `favicon.ico`) matcher: "/((?!_next|_vercel|monitoring|.*\\..*).*)", }; ``` And the @/libs/arcjet.ts file content is: ```javascript import arcjet, { shield } from "@arcjet/next"; // Create a base Arcjet instance which can be imported and extended in each route. export default arcjet({ // Get your site key from https://launch.arcjet.com/Q6eLbRE // Use `process.env` instead of Env to reduce bundle size in middleware key: process.env.ARCJET_KEY ?? "", // Identify the user by their IP address characteristics: ["ip.src"], rules: [ // Protect against common attacks with Arcjet Shield shield({ mode: "LIVE", // will block requests. Use "DRY_RUN" to log only }), // Other rules are added in different routes ], }); ``` # Conclusion We use Arcjet in the Next.js middleware, but it seems like it is including WASM files which are too large for the standard Hobby plan middleware limits on Vercel. Since quite a lot of developers are just experimenting on the Hobby plan on Vercel, I would suggest that it makes sense for Arcjet to make the package installed WASM files smaller or something similar. So the Arcjet product can still be used for expermentation in side projects. Otherwise the easiest logical step for a developer would likely be to remove Arcjet, as they prefereably want to keep their Hobby plan on Vercel.
1 条评论