Edge Runtime vs Node.js Runtime: Por qué tus funciones serverless fallan misteriosamente
Edge Runtime vs Node.js Runtime: Por qué tus funciones serverless fallan misteriosamente
Has escuchado la promesa: las funciones Edge son más rápidas, más baratas y se ejecutan más cerca de tus usuarios. Cold starts medidos en milisegundos en lugar de segundos. Distribución global por defecto. Así que agregas export const runtime = 'edge' a tu ruta API de Next.js, haces deploy, y... todo se rompe.
Bienvenido al infierno de depuración de Edge Runtime.
Los mensajes de error son crípticos. "Dynamic code evaluation not supported." "Module not found." "This API is not available." Tu código funcionaba perfectamente en desarrollo. Funcionaba en el runtime de Node.js. ¿Pero Edge? Edge tiene opiniones sobre lo que puedes y no puedes hacer.
Esto no es una crítica contra Edge Runtime—es genuinamente poderoso. Pero existe una enorme brecha de conocimiento entre el marketing de "Edge es rápido" y la brutal realidad de hacer que código de producción funcione en él. Esta guía llenará esa brecha.
La diferencia fundamental: Qué es realmente Edge Runtime
Antes de depurar, necesitamos entender con qué estamos trabajando. Edge Runtime y Node.js Runtime no son solo ubicaciones diferentes donde se ejecuta tu código—son entornos de ejecución fundamentalmente diferentes con diferentes APIs, diferentes restricciones y diferentes modelos mentales.
Node.js Runtime: Todo incluido
Node.js runtime es lo que conoces. Es un entorno Node.js completo ejecutándose en un servidor:
// Esto funciona en 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) { // Leer del sistema de archivos const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); // Usar crypto nativo const hash = crypto.createHash('sha256').update('secret').digest('hex'); // Crear procesos hijos const child = spawn('ls', ['-la']); // Usar cualquier paquete npm const pdf = await generatePDF(data); // Usa bindings nativos return Response.json({ success: true }); }
Node.js runtime te da:
- Acceso completo a módulos core de Node.js (
fs,path,crypto,child_process, etc.) - Soporte de módulos nativos (addons C++, bindings Rust via NAPI)
- Sin límites prácticos de tamaño de código
- Timeouts largos de ejecución (hasta 5 minutos en la mayoría de plataformas)
- Conexión a bases de datos via drivers nativos
- Depuración familiar con stack traces completos
La desventaja: cold starts de 250ms-1000ms+, despliegue regional (no global por defecto), y costos más altos a escala.
Edge Runtime: El velocista minimalista
Edge Runtime es completamente diferente. Está basado en Web APIs y V8 isolates—la misma tecnología que impulsa Cloudflare Workers:
// Esto es todo lo que tienes en Edge Runtime export const runtime = 'edge'; export async function POST(request) { // Web Fetch API - sí const response = await fetch('https://api.example.com/data'); // Web Crypto API - sí (¡pero diferente de Node crypto!) const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('secret') ); // Headers, Request, Response - sí const headers = new Headers(); headers.set('Cache-Control', 'max-age=3600'); // TextEncoder/TextDecoder - sí const encoded = new TextEncoder().encode('hello'); return new Response(JSON.stringify({ success: true }), { headers }); }
Edge Runtime te da:
- Solo Web Platform APIs (Fetch, Streams, Crypto, URL, etc.)
- Cold starts menores a 10ms
- Distribución global (ejecuta en 300+ ubicaciones)
- Costos menores a gran escala
- Límites estrictos de 1-4MB de código
- Timeouts cortos de ejecución (típicamente máximo 30 segundos)
La desventaja: sin filesystem, sin módulos nativos, sin módulos core de Node.js, y un subconjunto mucho menor de paquetes npm que realmente funcionan.
La taxonomía de fallos: Por qué tu código se rompe
Ahora categoricemos los fallos que encontrarás. Entender el tipo de fallo es el primer paso para arreglarlo.
Categoría 1: Módulos core de Node.js faltantes
El fallo más común. Importas algo que no existe en Edge:
// ❌ Estos fallan en Edge Runtime import fs from 'fs'; // Sin filesystem import path from 'path'; // Sin módulo path import crypto from 'crypto'; // API crypto diferente import { Buffer } from 'buffer'; // Soporte de Buffer limitado import stream from 'stream'; // Sin Node streams import http from 'http'; // Sin módulo http import https from 'https'; // Sin módulo https import net from 'net'; // Sin sockets TCP import dns from 'dns'; // Sin búsquedas DNS import child_process from 'child_process'; // Sin procesos import os from 'os'; // Sin info del OS import worker_threads from 'worker_threads'; // Sin threads
La solución: Reemplazar con equivalentes Web API o polyfills:
// ✅ Equivalentes Edge Runtime // En lugar 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(''); } // En lugar 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)) ); } // En lugar de path.join() function joinPath(...segments) { return segments.join('/').replace(/\/+/g, '/'); } // En lugar de parsear URL con querystring function parseQuery(url) { return Object.fromEntries(new URL(url).searchParams); }
Categoría 2: Evaluación de código dinámico
Edge Runtime prohíbe eval() y new Function() por razones de seguridad. Esto rompe más paquetes de los que esperarías:
// ❌ Todos estos fallan en Edge Runtime eval('console.log("hello")'); new Function('return 1 + 1')(); require('vm').runInNewContext('1 + 1'); // Tampoco hay módulo vm // Muchos paquetes usan esto internamente: // - Algunos motores de templates (Handlebars, EJS en ciertos modos) // - Algunos validadores de esquemas // - Algunas bibliotecas de serialización // - Procesamiento de source maps
El mensaje de error:
Dynamic code evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile')
not allowed in Edge Runtime
La solución: Encontrar paquetes alternativos o configurar bibliotecas para evitar evaluación dinámica:
// En lugar de lodash template con evaluación dinámica // ❌ Esto usa new Function internamente import template from 'lodash/template'; const compiled = template('Hello <%= name %>'); // ✅ Usa una biblioteca de templates estática import Mustache from 'mustache'; const output = Mustache.render('Hello {{name}}', { name: 'World' }); // O usa 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}`;
Categoría 3: Dependencias de módulos nativos
Cualquier paquete que use bindings nativos C++/Rust fallará en Edge:
// ❌ Estos paquetes no funcionan en Edge import sharp from 'sharp'; // Procesamiento de imágenes (nativo) import bcrypt from 'bcrypt'; // Hash de contraseñas (nativo) import canvas from 'canvas'; // Renderizado de canvas (nativo) import sqlite3 from 'sqlite3'; // SQLite (nativo) import puppeteer from 'puppeteer'; // Automatización de navegador (nativo) import prisma from '@prisma/client'; // Prisma (motor de queries nativo)
Los mensajes de error:
Error: Cannot find module 'sharp'
Module build failed: Native modules are not supported in Edge Runtime
La solución: Usar alternativas Edge-compatibles:
// ✅ Alternativas Edge-compatibles // En lugar de bcrypt (nativo) import { hash, compare } from 'bcryptjs'; // Implementación pura JS // En lugar de sharp (nativo) // Usa un servicio de imágenes en la nube o solución basada en 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; } // En lugar de Prisma con bindings nativos // Usa los adaptadores de driver Edge-compatibles de Prisma import { PrismaClient } from '@prisma/client'; import { PrismaNeon } from '@prisma/adapter-neon'; // O usa ORMs serverless-friendly import { drizzle } from 'drizzle-orm/neon-http'; // En lugar de SQLite // Usa D1 (Cloudflare), Turso, o PlanetScale
Categoría 4: Operaciones síncronas que bloquean
Edge Runtime está diseñado para operaciones rápidas y no bloqueantes. Cualquier cosa que bloquee el event loop es problemática:
// ❌ Estos patrones causan problemas en Edge import { readFileSync } from 'fs'; // Lectura síncrona (además: no hay fs) sleep(1000); // Sleep bloqueante // Computación síncrona pesada function fibonacciSync(n) { if (n <= 1) return n; return fibonacciSync(n - 1) + fibonacciSync(n - 2); } const result = fibonacciSync(45); // Bloquea por segundos // Parseo síncrono de JSON de payloads enormes const hugeData = JSON.parse(hugeJsonString); // Puede timeout
La solución: Usar streaming y procesamiento por chunks:
// ✅ Patrones Edge-friendly // Streamear respuestas grandes en lugar de bufferear 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 otras operaciones procedan await new Promise(resolve => setTimeout(resolve, 0)); } controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson' } }); } // Usar Web Streams para parsear 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); } } } }
Categoría 5: Violaciones de límite de tamaño
Las funciones Edge tienen límites de tamaño estrictos. Vercel Edge Functions: 1MB (gratis) a 4MB (Pro). Cloudflare Workers: 1MB (gratis) a 10MB (pago):
// ❌ Estos explotan el tamaño de tu bundle import _ from 'lodash'; // 70KB+ minificado import moment from 'moment'; // 300KB+ con locales import * as AWS from 'aws-sdk'; // Masivo import 'core-js/stable'; // 150KB+ de polyfills // Importar bibliotecas de iconos enteras import * as Icons from '@heroicons/react/24/solid'; // Modelos ML bundleados o datasets grandes import model from './large-ml-model.json'; // Modelo de 2MB
La solución: Tree shake agresivamente y lazy load:
// ✅ Imports optimizados para tamaño // En lugar de lodash completo import groupBy from 'lodash/groupBy'; import debounce from 'lodash/debounce'; // En lugar de moment import { format, parseISO } from 'date-fns'; // En lugar de aws-sdk v2 import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; // Imports de iconos específicos import { HomeIcon } from '@heroicons/react/24/solid'; // Fetch de datos grandes en runtime en lugar de bundlear export async function GET() { const model = await fetch('https://cdn.example.com/model.json').then(r => r.json()); // Usar model... }
Escenarios de depuración del mundo real
Veamos sesiones de depuración reales para fallos comunes de Edge Runtime.
Escenario 1: El misterioso "Module Not Found"
Síntoma: Tu app funciona localmente, compila exitosamente, pero falla en runtime en Edge.
Error: Cannot find module 'util'
at EdgeRuntime (edge-runtime.js:1:1)
Proceso de diagnóstico:
// Paso 1: Identificar qué está importando 'util' // Verifica tus dependencias recursivamente // Las dependencias en package.json pueden verse bien { "dependencies": { "next": "14.0.0", "jsonwebtoken": "9.0.0", // <-- ¡Este es el culpable! "next-auth": "4.24.0" } } // Paso 2: Verifica el árbol de dependencias // Ejecuta: npm ls util // Output: // └─┬ [email protected] // └── [email protected] // ¡jsonwebtoken usa el módulo util de Node internamente!
La solución:
// Opción 1: Usar una biblioteca JWT Edge-compatible import { SignJWT, jwtVerify } from 'jose'; // ¡Edge-compatible! export const runtime = 'edge'; export async function POST(request) { const secret = new TextEncoder().encode(process.env.JWT_SECRET); // Firmar 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 }); } // Opción 2: No usar Edge para esta ruta // Quitar: export const runtime = 'edge'; // Dejar que corra en Node.js runtime
Escenario 2: El desastre de sesiones de NextAuth.js
Síntoma: Auth funciona en desarrollo, se rompe en producción con Edge.
Error: next-auth requires a secret to be set in production
o
Error: [next-auth]: `useSession` must be wrapped in a <SessionProvider />
// (¡pero SÍ está envuelto, y funcionaba ayer!)
Diagnóstico:
// El problema: los route handlers de next-auth no son totalmente Edge-compatible // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; export const runtime = 'edge'; // ❌ ¡Esto rompe cosas! export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub], // Los adaptadores de base de datos también fallan en Edge adapter: PrismaAdapter(prisma), // ❌ ¡Dependencias nativas! });
La solución:
// app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import GitHub from 'next-auth/providers/github'; // Opción 1: Usar Node.js runtime para auth (recomendado por ahora) export const runtime = 'nodejs'; // ✅ Node.js explícito // Opción 2: Usar adaptadores Edge-compatible 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)), // ✅ Edge-compatible }); // El middleware SÍ puede usar Edge para verificaciones de auth (lectura, no escritura) // middleware.js export { auth as middleware } from './auth'; export const config = { matcher: ['/dashboard/:path*'] };
Escenario 3: La pesadilla de conexión a base de datos
Síntoma: Queries a la base de datos funcionan localmente, fallan en Edge en producción.
Error: Connection pool exhausted
Error: prepared statement "s0" already exists
Error: Cannot establish database connection
Diagnóstico:
// Las conexiones tradicionales de base de datos no funcionan en Edge // El problema: las funciones Edge son stateless y globalmente distribuidas import { Pool } from 'pg'; // ❌ Connection pooling falla en Edge const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20, // Esto no tiene sentido en Edge }); // ¡Cada invocación Edge está aislada - sin pool de conexiones compartido! // Estás intentando crear conexiones desde 300+ ubicaciones globales // a una sola región de base de datos - receta para el desastre
La solución:
// ✅ Patrones de base de datos Edge-compatibles // Opción 1: Conexiones de base de datos basadas en HTTP (mejor para Edge) import { neon } from '@neondatabase/serverless'; export const runtime = 'edge'; export async function GET() { const sql = neon(process.env.DATABASE_URL); // Cada query es una solicitud HTTP separada - sin conexión que gestionar const users = await sql`SELECT * FROM users LIMIT 10`; return Response.json({ users }); } // Opción 2: Prisma con Data Proxy import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.PRISMA_DATA_PROXY_URL, // Usar data proxy, no conexión directa }, }, }); // Opción 3: Para cargas pesadas de base de datos, no usar Edge export const runtime = 'nodejs'; // Aceptar el tradeoff de cold start import { db } from '@/lib/database'; export async function GET() { const users = await db.user.findMany(); return Response.json({ users }); }
El marco de decisión: Cuándo usar cada runtime
Después de todo este dolor de depuración, te preguntarás: ¿cuándo debería realmente usar Edge Runtime?
Usar Edge Runtime cuando:
1. La baja latencia es crítica y los datos son cacheables:
// ✅ Perfecto para Edge: Enrutamiento geográfico, contenido cacheado export const runtime = 'edge'; export async function GET(request) { const country = request.geo?.country || 'US'; // Servir contenido específico por región desde cache const content = await fetch(`https://cdn.example.com/content/${country}.json`, { next: { revalidate: 3600 } }); return Response.json(await content.json()); }
2. Verificaciones de autenticación (solo lectura):
// ✅ Perfecto para Edge: Validar JWTs, verificar permisos 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 y feature flags:
// ✅ Perfecto para Edge: Decisiones instantáneas, sin llamada al 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. Transformaciones simples y redirects:
// ✅ Perfecto para Edge: Reescritura de URLs, manipulación de headers export const runtime = 'edge'; export async function middleware(request) { const url = request.nextUrl.clone(); // Redirect URLs antiguas if (url.pathname.startsWith('/old-blog/')) { url.pathname = url.pathname.replace('/old-blog/', '/blog/'); return Response.redirect(url); } // Agregar headers de seguridad 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 cuando:
1. Operaciones de base de datos con connection pooling:
// ✅ Node.js: Bases de datos tradicionales que necesitan conexiones 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. Operaciones de archivos o procesamiento binario:
// ✅ Node.js: Cualquier cosa que toque filesystem o binarios 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 negocio compleja con muchas dependencias:
// ✅ Node.js: Cuando necesitas el ecosistema 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(); // Procesar pago const stripe = new Stripe(process.env.STRIPE_SECRET); const payment = await stripe.paymentIntents.create({...}); // Actualizar base de datos await prisma.order.update({...}); // Generar PDF de factura const pdf = await generatePDF(order); // Enviar email de confirmación await sendEmail({ to: order.email, subject: 'Confirmación de Pedido', attachments: [{ filename: 'invoice.pdf', content: pdf }], }); return Response.json({ success: true }); }
4. Operaciones de larga duración:
// ✅ Node.js: Cuando necesitas más de 30 segundos export const runtime = 'nodejs'; export const maxDuration = 300; // 5 minutos export async function POST(request) { const { videoUrl } = await request.json(); // Descargar y procesar video (toma 2-3 minutos) const processed = await processVideo(videoUrl); return Response.json({ downloadUrl: processed.url, duration: processed.duration }); }
El enfoque híbrido: Lo mejor de ambos mundos
La respuesta real no es "Edge vs Node.js"—es usar ambos estratégicamente:
src/
├── app/
│ ├── api/
│ │ ├── auth/ # Node.js - sesiones de base de datos
│ │ ├── payments/ # Node.js - Stripe, lógica compleja
│ │ ├── upload/ # Node.js - procesamiento de archivos
│ │ ├── geo/ # Edge - contenido basado en ubicación
│ │ ├── flags/ # Edge - feature flags
│ │ └── health/ # Edge - health checks rápidos
│ └── middleware.ts # Edge - verificaciones de auth, redirects
// middleware.ts - Corre en Edge en todas partes import { auth } from './auth'; export default auth((request) => { // Verificaciones de auth rápidas y globales 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 operaciones complejas export const runtime = 'nodejs'; export async function POST(request) { // Poder completo de Node.js para procesamiento de pedidos }
Conclusión
Edge Runtime es poderoso, pero no es magia. Es un entorno restringido que intercambia capacidades completas de Node.js por distribución global y cold starts menores a 10ms. Entender estas restricciones—módulos faltantes, sin evaluación dinámica, sin binarios nativos, límites de tamaño y ejecución stateless—es esencial para un despliegue Edge exitoso.
Puntos clave:
-
Edge es para operaciones rápidas y simples — Verificaciones de auth, redirects, tests A/B y entrega de contenido cacheado. No para lógica de negocio compleja.
-
En caso de duda, usa Node.js — La penalización de cold start (250ms-1000ms) a menudo es aceptable, y obtienes compatibilidad completa de API.
-
La estrategia de base de datos importa — Conexiones basadas en HTTP (Neon, PlanetScale HTTP, Prisma Data Proxy) para Edge, connection pools para Node.js.
-
Revisa tus dependencias — Ejecuta
npm lsen cualquier módulo core de Node para encontrar qué paquete lo está importando. Reemplaza con alternativas Edge-compatible. -
El tamaño importa — Tree shake agresivamente, lazy load datos, y usa imports específicos en lugar de barrel files.
-
Híbrido es la respuesta — Usa Edge para tu middleware y rutas críticas en latencia, Node.js para todo lo demás.
La próxima vez que tu función serverless falle misteriosamente después de agregar export const runtime = 'edge', sabrás exactamente dónde buscar. Edge Runtime no está roto—solo es diferente. Y una vez que entiendas esas diferencias, podrás aprovechar su poder sin caer en sus trampas.