Back

Edge Runtime vs Node.js Runtime: Por que suas funções serverless falham misteriosamente

Edge Runtime vs Node.js Runtime: Por que suas funções serverless falham misteriosamente

Você já ouviu a promessa: funções Edge são mais rápidas, mais baratas e rodam mais perto dos seus usuários. Cold starts medidos em milissegundos ao invés de segundos. Distribuição global por padrão. Então você adiciona export const runtime = 'edge' na sua rota API do Next.js, faz deploy, e... tudo quebra.

Bem-vindo ao inferno de depuração do Edge Runtime.

As mensagens de erro são crípticas. "Dynamic code evaluation not supported." "Module not found." "This API is not available." Seu código funcionava perfeitamente em desenvolvimento. Funcionava no runtime Node.js. Mas Edge? Edge tem opiniões sobre o que você pode e não pode fazer.

Isso não é uma crítica ao Edge Runtime—é genuinamente tecnologia poderosa. Mas existe uma enorme lacuna de conhecimento entre o marketing "Edge é rápido" e a realidade brutal de fazer código de produção rodar nele. Este guia vai preencher essa lacuna.

A diferença fundamental: O que é Edge Runtime realmente

Antes de depurar, precisamos entender com o que estamos trabalhando. Edge Runtime e Node.js Runtime não são apenas locais diferentes onde seu código roda—são ambientes de execução fundamentalmente diferentes com APIs diferentes, restrições diferentes e modelos mentais diferentes.

Node.js Runtime: O canivete suíço

Node.js runtime é o que você conhece. É um ambiente Node.js completo rodando em um servidor:

// Isso funciona no Node.js runtime import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; import { spawn } from 'child_process'; export async function POST(request) { // Ler do filesystem const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); // Usar crypto nativo const hash = crypto.createHash('sha256').update('secret').digest('hex'); // Spawnar processos filhos const child = spawn('ls', ['-la']); // Usar qualquer pacote npm const pdf = await generatePDF(data); // Usa bindings nativos return Response.json({ success: true }); }

Node.js runtime te dá:

  • Acesso completo aos módulos core do Node.js (fs, path, crypto, child_process, etc.)
  • Suporte a módulos nativos (addons C++, bindings Rust via NAPI)
  • Sem limites práticos de tamanho de código
  • Timeouts longos de execução (até 5 minutos na maioria das plataformas)
  • Conexão a bancos de dados via drivers nativos
  • Depuração familiar com stack traces completos

O tradeoff? Cold starts de 250ms-1000ms+, deploy regional (não global por padrão) e custos maiores em escala.

Edge Runtime: O velocista enxuto

Edge Runtime é completamente diferente. É baseado em Web APIs e V8 isolates—a mesma tecnologia que alimenta o Cloudflare Workers:

// Isso é tudo que você tem no Edge Runtime export const runtime = 'edge'; export async function POST(request) { // Web Fetch API - sim const response = await fetch('https://api.example.com/data'); // Web Crypto API - sim (mas diferente do crypto do Node!) const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('secret') ); // Headers, Request, Response - sim const headers = new Headers(); headers.set('Cache-Control', 'max-age=3600'); // TextEncoder/TextDecoder - sim const encoded = new TextEncoder().encode('hello'); return new Response(JSON.stringify({ success: true }), { headers }); }

Edge Runtime te dá:

  • Apenas Web Platform APIs (Fetch, Streams, Crypto, URL, etc.)
  • Cold starts menores que 10ms
  • Distribuição global (roda em 300+ locais)
  • Custos menores em grande escala
  • Limites estritos de 1-4MB de código
  • Timeouts curtos de execução (tipicamente máximo 30 segundos)

O tradeoff? Sem filesystem, sem módulos nativos, sem módulos core do Node.js, e um subconjunto muito menor de pacotes npm que realmente funcionam.

A taxonomia de falhas: Por que seu código quebra

Agora vamos categorizar as falhas que você vai encontrar. Entender o tipo de falha é o primeiro passo para consertar.

Categoria 1: Módulos core do Node.js ausentes

A falha mais comum. Você importa algo que não existe no Edge:

// ❌ Esses vão falhar no Edge Runtime import fs from 'fs'; // Sem filesystem import path from 'path'; // Sem módulo path import crypto from 'crypto'; // API crypto diferente import { Buffer } from 'buffer'; // Suporte limitado a Buffer import stream from 'stream'; // Sem Node streams import http from 'http'; // Sem módulo http import https from 'https'; // Sem módulo https import net from 'net'; // Sem sockets TCP import dns from 'dns'; // Sem lookups DNS import child_process from 'child_process'; // Sem processos import os from 'os'; // Sem info do OS import worker_threads from 'worker_threads'; // Sem threads

A solução: Substituir por equivalentes Web API ou polyfills:

// ✅ Equivalentes Edge Runtime // Ao invés de crypto.createHash() async function sha256(message) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Ao invés de Buffer.from() function base64Encode(str) { return btoa(String.fromCharCode(...new TextEncoder().encode(str))); } function base64Decode(base64) { return new TextDecoder().decode( Uint8Array.from(atob(base64), c => c.charCodeAt(0)) ); } // Ao invés de path.join() function joinPath(...segments) { return segments.join('/').replace(/\/+/g, '/'); } // Ao invés de parsear URL com querystring function parseQuery(url) { return Object.fromEntries(new URL(url).searchParams); }

Categoria 2: Avaliação de código dinâmico

Edge Runtime proíbe eval() e new Function() por razões de segurança. Isso quebra mais pacotes do que você esperaria:

// ❌ Todos esses vão falhar no Edge Runtime eval('console.log("hello")'); new Function('return 1 + 1')(); require('vm').runInNewContext('1 + 1'); // Também sem módulo vm // Muitos pacotes usam isso internamente: // - Alguns engines de template (Handlebars, EJS em certos modos) // - Alguns validadores de schema // - Algumas bibliotecas de serialização // - Processamento de source maps

A mensagem de erro:

Dynamic code evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile') 
not allowed in Edge Runtime

A solução: Encontrar pacotes alternativos ou configurar bibliotecas para evitar avaliação dinâmica:

// Ao invés de lodash template com avaliação dinâmica // ❌ Isso usa new Function internamente import template from 'lodash/template'; const compiled = template('Hello <%= name %>'); // ✅ Use uma biblioteca de templates estática import Mustache from 'mustache'; const output = Mustache.render('Hello {{name}}', { name: 'World' }); // Ou use tagged template literals function html(strings, ...values) { return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '' ); } const name = 'World'; const output = html`Hello ${name}`;

Categoria 3: Dependências de módulos nativos

Qualquer pacote que use bindings nativos C++/Rust vai falhar no Edge:

// ❌ Esses pacotes não funcionam no Edge import sharp from 'sharp'; // Processamento de imagens (nativo) import bcrypt from 'bcrypt'; // Hash de senhas (nativo) import canvas from 'canvas'; // Renderização de canvas (nativo) import sqlite3 from 'sqlite3'; // SQLite (nativo) import puppeteer from 'puppeteer'; // Automação de navegador (nativo) import prisma from '@prisma/client'; // Prisma (engine de queries nativo)

As mensagens de erro:

Error: Cannot find module 'sharp'
Module build failed: Native modules are not supported in Edge Runtime

A solução: Usar alternativas compatíveis com Edge:

// ✅ Alternativas compatíveis com Edge // Ao invés de bcrypt (nativo) import { hash, compare } from 'bcryptjs'; // Implementação JS pura // Ao invés de sharp (nativo) // Use um serviço de imagens na nuvem ou solução baseada em WebAssembly async function resizeImage(imageUrl, width, height) { const response = await fetch( `https://images.example.com/resize?url=${encodeURIComponent(imageUrl)}&w=${width}&h=${height}` ); return response; } // Ao invés de Prisma com bindings nativos // Use os adaptadores de driver compatíveis com Edge do Prisma import { PrismaClient } from '@prisma/client'; import { PrismaNeon } from '@prisma/adapter-neon'; // Ou use ORMs serverless-friendly import { drizzle } from 'drizzle-orm/neon-http'; // Ao invés de SQLite // Use D1 (Cloudflare), Turso, ou PlanetScale

Categoria 4: Operações síncronas que bloqueiam

Edge Runtime é projetado para operações rápidas e não-bloqueantes. Qualquer coisa que bloqueie o event loop é problemática:

// ❌ Esses padrões causam problemas no Edge import { readFileSync } from 'fs'; // Leitura síncrona (e também: sem fs) sleep(1000); // Sleep bloqueante // Computação síncrona pesada function fibonacciSync(n) { if (n <= 1) return n; return fibonacciSync(n - 1) + fibonacciSync(n - 2); } const result = fibonacciSync(45); // Bloqueia por segundos // Parsing síncrono de JSON de payloads enormes const hugeData = JSON.parse(hugeJsonString); // Pode dar timeout

A solução: Usar streaming e processamento em chunks:

// ✅ Padrões Edge-friendly // Streamear respostas grandes ao invés de bufferizar export async function GET() { const stream = new ReadableStream({ async start(controller) { for (let i = 0; i < 1000; i++) { const chunk = await fetchChunk(i); controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk) + '\n')); // Permitir que outras operações procedam await new Promise(resolve => setTimeout(resolve, 0)); } controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } }); } // Usar Web Streams para parsing async function* parseJsonStream(response) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (line.trim()) { yield JSON.parse(line); } } } }

Categoria 5: Violações de limite de tamanho

Funções Edge têm limites de tamanho estritos. Vercel Edge Functions: 1MB (grátis) a 4MB (Pro). Cloudflare Workers: 1MB (grátis) a 10MB (pago):

// ❌ Esses explodem o tamanho do seu bundle import _ from 'lodash'; // 70KB+ minificado import moment from 'moment'; // 300KB+ com locales import * as AWS from 'aws-sdk'; // Massivo import 'core-js/stable'; // 150KB+ de polyfills // Importar bibliotecas de ícones inteiras import * as Icons from '@heroicons/react/24/solid'; // Modelos ML ou datasets grandes bundleados import model from './large-ml-model.json'; // Modelo de 2MB

A solução: Tree shake agressivamente e fazer lazy load:

// ✅ Imports otimizados para tamanho // Ao invés de lodash completo import groupBy from 'lodash/groupBy'; import debounce from 'lodash/debounce'; // Ao invés de moment import { format, parseISO } from 'date-fns'; // Ao invés de aws-sdk v2 import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; // Imports de ícones específicos import { HomeIcon } from '@heroicons/react/24/solid'; // Buscar dados grandes em runtime ao invés de bundlear export async function GET() { const model = await fetch('https://cdn.example.com/model.json').then(r => r.json()); // Usar model... }

Cenários de depuração do mundo real

Vamos passar por sessões de depuração reais para falhas comuns do Edge Runtime.

Cenário 1: O misterioso "Module Not Found"

Sintoma: Sua app funciona localmente, builda com sucesso, mas falha em runtime no Edge.

Error: Cannot find module 'util'
    at EdgeRuntime (edge-runtime.js:1:1)

Processo de diagnóstico:

// Passo 1: Identificar o que está importando 'util' // Verifique suas dependências recursivamente // Dependências do package.json podem parecer ok { "dependencies": { "next": "14.0.0", "jsonwebtoken": "9.0.0", // <-- Esse é o culpado! "next-auth": "4.24.0" } } // Passo 2: Verifique a árvore de dependências // Execute: npm ls util // Output: // └─┬ [email protected] // └── [email protected] // jsonwebtoken usa o módulo util do Node internamente!

A solução:

// Opção 1: Usar uma lib JWT compatível com Edge import { SignJWT, jwtVerify } from 'jose'; // Compatível com Edge! export const runtime = 'edge'; export async function POST(request) { const secret = new TextEncoder().encode(process.env.JWT_SECRET); // Assinar const token = await new SignJWT({ userId: '123' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(secret); // Verificar const { payload } = await jwtVerify(token, secret); return Response.json({ token, payload }); } // Opção 2: Não usar Edge para essa rota // Remover: export const runtime = 'edge'; // Deixar rodar no Node.js runtime

Cenário 2: O desastre de sessões do NextAuth.js

Sintoma: Auth funciona em desenvolvimento, quebra em produção com Edge.

Error: next-auth requires a secret to be set in production

ou

Error: [next-auth]: `useSession` must be wrapped in a <SessionProvider />
// (mas EStÁ wrapped, e funcionava ontem!)

Diagnóstico:

// O problema: os route handlers do next-auth não são totalmente compatíveis com Edge // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; export const runtime = 'edge'; // ❌ Isso quebra coisas! export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub], // Adaptadores de banco também falham no Edge adapter: PrismaAdapter(prisma), // ❌ Dependências nativas! });

A solução:

// app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; // Opção 1: Usar Node.js runtime para auth (recomendado por enquanto) export const runtime = 'nodejs'; // ✅ Node.js explícito // Opção 2: Usar adaptadores compatíveis com Edge import { DrizzleAdapter } from '@auth/drizzle-adapter'; import { drizzle } from 'drizzle-orm/neon-http'; export const { handlers, auth } = NextAuth({ providers: [GitHub], adapter: DrizzleAdapter(drizzle(process.env.DATABASE_URL)), // ✅ Compatível com Edge }); // Middleware PODE usar Edge para verificações de auth (leitura, não escrita) // middleware.js export { auth as middleware } from './auth'; export const config = { matcher: ['/dashboard/:path*'] };

Cenário 3: O pesadelo da conexão com banco de dados

Sintoma: Queries no banco funcionam localmente, falham no Edge em produção.

Error: Connection pool exhausted
Error: prepared statement "s0" already exists
Error: Cannot establish database connection

Diagnóstico:

// Conexões tradicionais de banco não funcionam no Edge // O problema: funções Edge são stateless e globalmente distribuídas import { Pool } from 'pg'; // ❌ Connection pooling quebra no Edge const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Isso não faz sentido no Edge }); // Cada invocação Edge é isolada - sem pool de conexões compartilhado! // Você está tentando criar conexões de 300+ locais globais // para uma única região de banco - receita para desastre

A solução:

// ✅ Padrões de banco de dados compatíveis com Edge // Opção 1: Conexões de banco baseadas em HTTP (melhor para Edge) import { neon } from '@neondatabase/serverless'; export const runtime = 'edge'; export async function GET() { const sql = neon(process.env.DATABASE_URL); // Cada query é uma requisição HTTP separada - sem conexão para gerenciar const users = await sql`SELECT * FROM users LIMIT 10`; return Response.json({ users }); } // Opção 2: Prisma com Data Proxy import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.PRISMA_DATA_PROXY_URL, // Usar data proxy, não conexão direta }, }, }); // Opção 3: Para cargas pesadas de banco, não usar Edge export const runtime = 'nodejs'; // Aceitar o tradeoff de cold start import { db } from '@/lib/database'; export async function GET() { const users = await db.user.findMany(); return Response.json({ users }); }

O framework de decisão: Quando usar cada runtime

Depois de toda essa dor de depuração, você pode estar se perguntando: quando devo realmente usar Edge Runtime?

Usar Edge Runtime quando:

1. Baixa latência é crítica e dados são cacheáveis:

// ✅ Perfeito para Edge: Roteamento geográfico, conteúdo cacheado export const runtime = 'edge'; export async function GET(request) { const country = request.geo?.country || 'US'; // Servir conteúdo específico por região do cache const content = await fetch(`https://cdn.example.com/content/${country}.json`, { next: { revalidate: 3600 } }); return Response.json(await content.json()); }

2. Verificações de autenticação (somente leitura):

// ✅ Perfeito para Edge: Validar JWTs, verificar permissões export const runtime = 'edge'; export async function middleware(request) { const token = request.cookies.get('session'); if (!token) { return Response.redirect(new URL('/login', request.url)); } try { await jwtVerify(token.value, secret); return NextResponse.next(); } catch { return Response.redirect(new URL('/login', request.url)); } }

3. A/B testing e feature flags:

// ✅ Perfeito para Edge: Decisões instantâneas, sem chamada ao backend export const runtime = 'edge'; export async function middleware(request) { const bucket = Math.random(); const variant = bucket < 0.5 ? 'control' : 'treatment'; const response = NextResponse.next(); response.cookies.set('experiment_variant', variant); return response; }

4. Transformações simples e redirects:

// ✅ Perfeito para Edge: Reescrita de URLs, manipulação de headers export const runtime = 'edge'; export async function middleware(request) { const url = request.nextUrl.clone(); // Redirect URLs antigas if (url.pathname.startsWith('/old-blog/')) { url.pathname = url.pathname.replace('/old-blog/', '/blog/'); return Response.redirect(url); } // Adicionar headers de segurança const response = NextResponse.next(); response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); return response; }

Usar Node.js Runtime quando:

1. Operações de banco de dados com connection pooling:

// ✅ Node.js: Bancos tradicionais que precisam de conexões persistentes export const runtime = 'nodejs'; import { prisma } from '@/lib/prisma'; export async function GET() { const users = await prisma.user.findMany({ include: { posts: true }, take: 50, }); return Response.json({ users }); }

2. Operações de arquivo ou processamento binário:

// ✅ Node.js: Qualquer coisa que toque filesystem ou binários nativos export const runtime = 'nodejs'; import sharp from 'sharp'; import { writeFile } from 'fs/promises'; export async function POST(request) { const formData = await request.formData(); const file = formData.get('image'); const buffer = Buffer.from(await file.arrayBuffer()); const processed = await sharp(buffer) .resize(800, 600) .webp({ quality: 80 }) .toBuffer(); await writeFile(`./uploads/${file.name}.webp`, processed); return Response.json({ success: true }); }

3. Lógica de negócios complexa com muitas dependências:

// ✅ Node.js: Quando você precisa do ecossistema npm completo export const runtime = 'nodejs'; import Stripe from 'stripe'; import { sendEmail } from '@/lib/email'; // Usa nodemailer import { generatePDF } from '@/lib/pdf'; // Usa puppeteer import { prisma } from '@/lib/prisma'; export async function POST(request) { const order = await request.json(); // Processar pagamento const stripe = new Stripe(process.env.STRIPE_SECRET); const payment = await stripe.paymentIntents.create({...}); // Atualizar banco await prisma.order.update({...}); // Gerar PDF da fatura const pdf = await generatePDF(order); // Enviar email de confirmação await sendEmail({ to: order.email, subject: 'Confirmação de Pedido', attachments: [{ filename: 'invoice.pdf', content: pdf }], }); return Response.json({ success: true }); }

4. Operações de longa duração:

// ✅ Node.js: Quando você precisa de mais de 30 segundos export const runtime = 'nodejs'; export const maxDuration = 300; // 5 minutos export async function POST(request) { const { videoUrl } = await request.json(); // Download e processamento de vídeo (leva 2-3 minutos) const processed = await processVideo(videoUrl); return Response.json({ downloadUrl: processed.url, duration: processed.duration }); }

A abordagem híbrida: O melhor dos dois mundos

A resposta real não é "Edge vs Node.js"—é usar ambos estrategicamente:

src/
├── app/
│   ├── api/
│   │   ├── auth/         # Node.js - sessões de banco
│   │   ├── payments/     # Node.js - Stripe, lógica complexa
│   │   ├── upload/       # Node.js - processamento de arquivos
│   │   ├── geo/          # Edge - conteúdo baseado em localização
│   │   ├── flags/        # Edge - feature flags
│   │   └── health/       # Edge - health checks rápidos
│   └── middleware.ts     # Edge - verificações de auth, redirects
// middleware.ts - Roda no Edge em todo lugar import { auth } from './auth'; export default auth((request) => { // Verificações de auth rápidas e globais if (!request.auth && request.nextUrl.pathname.startsWith('/dashboard')) { return Response.redirect(new URL('/login', request.url)); } }); export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
// app/api/geo/route.ts - Edge para lookups geográficos rápidos export const runtime = 'edge'; export async function GET(request) { const { country, city, latitude, longitude } = request.geo || {}; const nearestStore = await fetch( `https://api.stores.com/nearest?lat=${latitude}&lng=${longitude}`, { next: { revalidate: 3600 } } // Cache por 1 hora ).then(r => r.json()); return Response.json({ nearestStore, userLocation: { country, city } }); }
// app/api/orders/route.ts - Node.js para operações complexas export const runtime = 'nodejs'; export async function POST(request) { // Poder completo do Node.js para processamento de pedidos }

Conclusão

Edge Runtime é poderoso, mas não é mágica. É um ambiente restrito que troca capacidades completas do Node.js por distribuição global e cold starts menores que 10ms. Entender essas restrições—módulos ausentes, sem avaliação dinâmica, sem binários nativos, limites de tamanho e execução stateless—é essencial para um deploy Edge bem-sucedido.

Pontos-chave:

  1. Edge é para operações rápidas e simples — Verificações de auth, redirects, testes A/B e entrega de conteúdo cacheado. Não para lógica de negócios complexa.

  2. Na dúvida, use Node.js — A penalidade de cold start (250ms-1000ms) geralmente é aceitável, e você ganha compatibilidade completa de API.

  3. Estratégia de banco importa — Conexões baseadas em HTTP (Neon, PlanetScale HTTP, Prisma Data Proxy) para Edge, connection pools para Node.js.

  4. Verifique suas dependências — Rode npm ls em qualquer módulo core do Node para achar qual pacote está importando. Substitua por alternativas compatíveis com Edge.

  5. Tamanho importa — Tree shake agressivamente, faça lazy load de dados, e use imports específicos ao invés de barrel files.

  6. Híbrido é a resposta — Use Edge para seu middleware e rotas críticas em latência, Node.js para todo o resto.

Na próxima vez que sua função serverless falhar misteriosamente depois de adicionar export const runtime = 'edge', você vai saber exatamente onde olhar. Edge Runtime não está quebrado—é apenas diferente. E uma vez que você entenda essas diferenças, você pode aproveitar seu poder sem cair nas armadilhas.

edge-runtimenodejsserverlessnextjsvercelcloudflare-workersdebugging