Ir para o conteúdo
·18 min de leitura

Edge Functions: O Que São, Quando Usar e Quando Não Usar

Edge runtime, V8 isolates, o mito do cold start, geo-routing, testes A/B, auth na edge e por que eu movi algumas coisas de volta para Node.js. Uma visão equilibrada de edge computing.

Compartilhar:X / TwitterLinkedIn

A palavra "edge" é usada o tempo todo. A Vercel fala. A Cloudflare fala. A Deno fala. Toda palestra de conferência sobre performance web inevitavelmente menciona "rodar na edge" como se fosse um encantamento mágico que torna sua app rápida.

Eu comprei a ideia. Movi middleware, rotas de API, até alguma lógica de renderização para o edge runtime. Algumas dessas mudanças foram brilhantes. Outras eu silenciosamente movi de volta para Node.js três semanas depois, após debugar erros de connection pool às 2 da manhã.

Este post é a versão equilibrada dessa história — o que a edge realmente é, onde ela genuinamente brilha, onde absolutamente não funciona, e como eu decido qual runtime usar para cada parte da minha aplicação.

O Que É a Edge?#

Vamos começar pela geografia. Quando alguém visita seu site, a requisição viaja do dispositivo, pelo ISP, pela internet até seu servidor, é processada, e a resposta viaja todo o caminho de volta. Se seu servidor está em us-east-1 (Virginia) e seu usuário está em Tóquio, essa viagem de ida e volta cobre aproximadamente 14.000 km. À velocidade da luz pela fibra, isso é cerca de 70ms só pela física — um sentido. Adicione resolução DNS, handshake TLS e qualquer tempo de processamento, e facilmente estamos olhando para 200-400ms antes do seu usuário ver um único byte.

A "edge" significa rodar seu código em servidores distribuídos globalmente — os mesmos nós CDN que sempre serviram assets estáticos, mas agora eles também podem executar sua lógica. Em vez de um servidor de origem na Virginia, seu código roda em 300+ localizações mundiais. Um usuário em Tóquio acessa um servidor em Tóquio. Um usuário em Paris acessa um servidor em Paris.

A matemática de latência é simples e convincente:

Tradicional (origem única):
  Tóquio → Virginia: ~140ms ida e volta (só física)
  + Handshake TLS: ~140ms a mais (outra ida e volta)
  + Processamento: 20-50ms
  Total: ~300-330ms

Edge (PoP local):
  Tóquio → Nó edge em Tóquio: ~5ms ida e volta
  + Handshake TLS: ~5ms a mais
  + Processamento: 5-20ms
  Total: ~15-30ms

Isso é uma melhoria de 10-20x para a resposta inicial. É real, é mensurável, e para certas operações é transformador.

Mas aqui está o que o marketing omite: a edge não é um ambiente de servidor completo. É algo fundamentalmente diferente.

V8 Isolates vs Node.js#

O Node.js tradicional roda em um processo de sistema operacional completo. Tem acesso ao sistema de arquivos, pode abrir conexões TCP, pode criar processos filhos, pode ler variáveis de ambiente como um stream, pode fazer essencialmente qualquer coisa que um processo Linux pode fazer.

Edge functions não rodam em Node.js. Rodam em V8 isolates — o mesmo motor JavaScript que alimenta o Chrome, mas reduzido ao seu núcleo. Pense em um V8 isolate como uma sandbox leve:

typescript
// Isso funciona no Node.js mas NÃO na edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
 
const file = fs.readFileSync("/etc/hosts");        // ❌ Sem sistema de arquivos
const conn = createConnection({ port: 5432 });     // ❌ Sem TCP bruto
const result = execSync("ls -la");                  // ❌ Sem processos filhos
process.env.DATABASE_URL;                           // ⚠️  Disponível mas estático, definido no deploy

O que você TEM na edge é a superfície da Web API — as mesmas APIs disponíveis em um navegador:

typescript
// Tudo isso funciona na edge
const response = await fetch("https://api.example.com/data");
const url = new URL(request.url);
const headers = new Headers({ "Content-Type": "application/json" });
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
const hash = await crypto.subtle.digest("SHA-256", encoded);
const id = crypto.randomUUID();
 
// Web Streams API
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("chunk 1");
    controller.enqueue("chunk 2");
    controller.close();
  },
});
 
// Cache API
const cache = caches.default;
await cache.put(request, response.clone());

As restrições são reais e severas:

  • Memória: 128MB por isolate (Cloudflare Workers), 256MB em algumas plataformas
  • Tempo de CPU: 10-50ms de tempo real de CPU (não tempo de relógio — await fetch() não conta, mas JSON.parse() em um payload de 5MB conta)
  • Sem módulos nativos: Qualquer coisa que precise de binding C++ (bcrypt, sharp, canvas) está fora
  • Sem conexões persistentes: Você não pode manter uma conexão de banco de dados aberta entre requisições
  • Limites de tamanho de bundle: Tipicamente 1-5MB para o script worker inteiro

Isso não é Node.js em uma CDN. É um runtime diferente com um modelo mental diferente.

Cold Starts: O Mito e a Realidade#

Você provavelmente ouviu que edge functions têm "zero cold starts." Isso é... quase verdade, e a comparação é genuinamente dramática.

Uma função serverless tradicional baseada em container (AWS Lambda, Google Cloud Functions) funciona assim:

  1. Requisição chega
  2. Plataforma provisiona um container (se nenhum disponível)
  3. Container inicia o SO
  4. Runtime inicializa (Node.js, Python, etc.)
  5. Seu código carrega e inicializa
  6. Requisição é processada

Os passos 2-5 são o cold start. Para um Lambda Node.js, isso é tipicamente 200-500ms. Para um Lambda Java, pode ser 2-5 segundos. Para um Lambda .NET, 500ms-1.5s.

V8 isolates funcionam diferente:

  1. Requisição chega
  2. Plataforma cria um novo V8 isolate (ou reutiliza um aquecido)
  3. Seu código carrega (já está compilado em bytecode no momento do deploy)
  4. Requisição é processada

Os passos 2-3 levam menos de 5ms. Frequentemente menos de 1ms. O isolate não é um container — não há SO para iniciar, não há runtime para inicializar. O V8 cria um isolate fresco em microsegundos. A frase "zero cold start" é jargão de marketing, mas a realidade (startup sub-5ms) é próxima o suficiente de zero que não importa para a maioria dos casos de uso.

Mas aqui é quando cold starts ainda mordem na edge:

Bundles grandes. Se sua edge function puxa 2MB de dependências, esse código ainda precisa ser carregado e parseado. Aprendi isso da maneira difícil quando bundlei uma biblioteca de validação e uma de formatação de data em um edge middleware. O cold start foi de 2ms para 40ms. Ainda rápido, mas não "zero."

Localizações raras. Provedores de edge têm centenas de PoPs, mas nem todos mantêm seu código aquecido. Se você recebe uma requisição por hora de Nairobi, aquele isolate é reciclado entre requisições. A próxima requisição paga o custo de startup novamente.

Múltiplos isolates por requisição. Se sua edge function chama outra edge function (ou se middleware e uma rota de API são ambos edge), você pode estar criando múltiplos isolates para uma requisição de usuário.

O conselho prático: mantenha seus bundles de edge function pequenos. Importe apenas o necessário. Faça tree-shake agressivamente. Quanto menor o bundle, mais rápido o cold start, mais a promessa de "zero cold start" se sustenta.

typescript
// ❌ Não faça isso na edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
 
// ✅ Faça isso em vez — use APIs nativas
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];

Casos de Uso Perfeitos para Edge Functions#

Após experimentar extensivamente, encontrei um padrão claro: edge functions se destacam quando você precisa tomar uma decisão rápida sobre uma requisição antes que ela alcance seu servidor de origem. São guardiões, roteadores e transformadores — não servidores de aplicação.

1. Redirecionamentos Baseados em Geolocalização#

Este é o caso de uso matador. A requisição atinge o nó edge mais próximo, que já sabe onde o usuário está. Sem necessidade de chamada de API, sem banco de dados de lookup de IP — a plataforma fornece os dados geográficos:

typescript
// middleware.ts — roda na edge em cada requisição
import { NextRequest, NextResponse } from "next/server";
 
export const config = {
  matcher: ["/", "/shop/:path*"],
};
 
export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? "US";
  const city = request.geo?.city ?? "Unknown";
  const region = request.geo?.region ?? "Unknown";
 
  // Redirecionar para loja específica do país
  if (request.nextUrl.pathname === "/shop") {
    const storeMap: Record<string, string> = {
      DE: "/shop/eu",
      FR: "/shop/eu",
      GB: "/shop/uk",
      JP: "/shop/jp",
      TR: "/shop/tr",
    };
 
    const storePath = storeMap[country] ?? "/shop/us";
    if (request.nextUrl.pathname !== storePath) {
      return NextResponse.redirect(new URL(storePath, request.url));
    }
  }
 
  // Adicionar headers geo para uso downstream
  const response = NextResponse.next();
  response.headers.set("x-user-country", country);
  response.headers.set("x-user-city", city);
  response.headers.set("x-user-region", region);
  return response;
}

Isso roda em menos de 5ms, bem perto do usuário. A alternativa — enviar a requisição todo o caminho até seu servidor de origem só para fazer um lookup de IP e redirecionar de volta — custaria 100-300ms para usuários distantes da sua origem.

2. Testes A/B Sem Flicker no Cliente#

Testes A/B no lado do cliente causam o temido "flash de conteúdo original" — o usuário vê a versão A por uma fração de segundo antes do JavaScript trocar para a versão B. Na edge, você pode atribuir a variante antes da página sequer começar a renderizar:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Verificar se o usuário já tem uma atribuição de variante
  const existingVariant = request.cookies.get("ab-variant")?.value;
 
  if (existingVariant) {
    // Reescrever para a página de variante correta
    const url = request.nextUrl.clone();
    url.pathname = `/variants/${existingVariant}${url.pathname}`;
    return NextResponse.rewrite(url);
  }
 
  // Atribuir uma nova variante (divisão 50/50)
  const variant = Math.random() < 0.5 ? "control" : "treatment";
 
  const url = request.nextUrl.clone();
  url.pathname = `/variants/${variant}${url.pathname}`;
 
  const response = NextResponse.rewrite(url);
  response.cookies.set("ab-variant", variant, {
    maxAge: 60 * 60 * 24 * 30, // 30 dias
    httpOnly: true,
    sameSite: "lax",
  });
 
  return response;
}

O usuário nunca vê um flicker porque o rewrite acontece no nível de rede. O navegador nem sabe que foi um teste A/B — simplesmente recebe a página da variante diretamente.

3. Verificação de Token de Auth#

Se sua auth usa JWTs (e você não está fazendo lookups de sessão no banco de dados), a edge é perfeita. Verificação de JWT é pura criptografia — sem banco de dados necessário:

typescript
import { jwtVerify, importSPKI } from "jose";
import { NextRequest, NextResponse } from "next/server";
 
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
 
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;
 
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  try {
    const key = await importSPKI(PUBLIC_KEY, "RS256");
    const { payload } = await jwtVerify(token, key, {
      algorithms: ["RS256"],
      issuer: "https://auth.example.com",
    });
 
    // Passar informações do usuário downstream como headers
    const response = NextResponse.next();
    response.headers.set("x-user-id", payload.sub as string);
    response.headers.set("x-user-role", payload.role as string);
    return response;
  } catch {
    // Token expirado ou inválido
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("session-token");
    return response;
  }
}

Este padrão é poderoso: o edge middleware verifica o token e passa informações do usuário para sua origem como headers confiáveis. Suas rotas de API não precisam verificar o token novamente — simplesmente leem request.headers.get("x-user-id").

4. Detecção de Bots e Rate Limiting#

Edge functions podem bloquear tráfego indesejado antes que ele alcance sua origem:

typescript
import { NextRequest, NextResponse } from "next/server";
 
// Rate limiter simples em memória (por localização edge)
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
 
export function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim()
    ?? "unknown";
  const ua = request.headers.get("user-agent") ?? "";
 
  // Bloquear bots maliciosos conhecidos
  const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
  if (badBots.some((bot) => ua.includes(bot))) {
    return new NextResponse("Forbidden", { status: 403 });
  }
 
  // Rate limiting simples
  const now = Date.now();
  const windowMs = 60_000; // 1 minuto
  const maxRequests = 100;
 
  const entry = rateLimitMap.get(ip);
  if (entry && now - entry.timestamp < windowMs) {
    entry.count++;
    if (entry.count > maxRequests) {
      return new NextResponse("Too Many Requests", {
        status: 429,
        headers: { "Retry-After": "60" },
      });
    }
  } else {
    rateLimitMap.set(ip, { count: 1, timestamp: now });
  }
 
  // Limpeza periódica para evitar memory leak
  if (rateLimitMap.size > 10_000) {
    const cutoff = now - windowMs;
    for (const [key, val] of rateLimitMap) {
      if (val.timestamp < cutoff) rateLimitMap.delete(key);
    }
  }
 
  return NextResponse.next();
}

Uma ressalva: o mapa de rate limit acima é por isolate, por localização. Se você tem 300 localizações edge, cada uma tem seu próprio mapa. Para rate limiting estrito, você precisa de um store distribuído como Upstash Redis ou Cloudflare Durable Objects. Mas para prevenção básica de abuso, limites por localização funcionam surpreendentemente bem.

5. Reescrita de Requisições e Headers de Personalização#

Edge functions são excelentes em transformar requisições antes que cheguem à sua origem:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl;
 
  // Negociação de conteúdo baseada em dispositivo
  const ua = request.headers.get("user-agent") ?? "";
  const isMobile = /mobile|android|iphone/i.test(ua);
  response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
 
  // Feature flags do cookie
  const flags = request.cookies.get("feature-flags")?.value;
  if (flags) {
    response.headers.set("x-feature-flags", flags);
  }
 
  // Detecção de locale para i18n
  const acceptLanguage = request.headers.get("accept-language") ?? "en";
  const preferredLocale = acceptLanguage.split(",")[0]?.split("-")[0] ?? "en";
  const supportedLocales = [
    "en", "tr", "de", "fr", "es", "pt", "ja", "ko", "it",
    "nl", "ru", "pl", "uk", "sv", "cs", "ar", "hi", "zh",
  ];
  const locale = supportedLocales.includes(preferredLocale)
    ? preferredLocale
    : "en";
 
  if (!url.pathname.startsWith(`/${locale}`) && !url.pathname.startsWith("/api")) {
    return NextResponse.redirect(new URL(`/${locale}${url.pathname}`, request.url));
  }
 
  return response;
}

Onde a Edge Falha#

Esta é a seção que as páginas de marketing pulam. Eu bati em cada uma dessas paredes.

1. Conexões de Banco de Dados#

Esta é a grande. Bancos de dados tradicionais (PostgreSQL, MySQL) usam conexões TCP persistentes. Um servidor Node.js abre um pool de conexões na inicialização e reutiliza essas conexões entre requisições. Eficiente, comprovado, bem entendido.

Edge functions não podem fazer isso. Cada isolate é efêmero. Não há fase de "startup" onde você abre conexões. Mesmo que pudesse abrir uma conexão, o isolate pode ser reciclado após uma requisição, desperdiçando o tempo de setup da conexão.

typescript
// ❌ Este padrão fundamentalmente não funciona na edge
import { Pool } from "pg";
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // Pool de 10 conexões
});
 
// Cada invocação edge iria:
// 1. Criar um novo Pool (não pode reutilizar entre invocações de forma confiável)
// 2. Abrir uma conexão TCP com seu banco (que está em us-east-1, não na edge)
// 3. Fazer handshake TLS com o banco
// 4. Rodar a query
// 5. Descartar a conexão quando o isolate é reciclado
 
// Mesmo com serviços de connection pooling como PgBouncer,
// você ainda paga a latência de rede da edge → banco de dados na origem

O problema de ida e volta ao banco é fundamental. Seu banco está em uma região. Sua edge function está em 300 regiões. Cada query de banco da edge precisa viajar da localização edge até a região do banco e voltar. Para um usuário em Tóquio acessando um nó edge em Tóquio, mas seu banco está na Virginia:

Edge function em Tóquio
  → Query ao PostgreSQL na Virginia: ~140ms ida e volta
  → Segunda query: ~140ms a mais
  → Total: 280ms só para duas queries

Função Node.js na Virginia (mesma região do BD)
  → Query ao PostgreSQL: ~1ms ida e volta
  → Segunda query: ~1ms a mais
  → Total: 2ms para duas queries

A edge function é 140x mais lenta para operações de banco neste cenário. Não importa que a edge function iniciou mais rápido — as idas e voltas ao banco dominam tudo.

É por isso que proxies de banco baseados em HTTP existem (driver serverless da Neon, driver baseado em fetch da PlanetScale, API REST da Supabase). Funcionam, mas ainda fazem requisições HTTP para um banco em uma única região. Resolvem o problema "não consigo usar TCP" mas não o problema "o banco está longe".

typescript
// ✅ Isso funciona na edge (acesso a banco baseado em HTTP)
// Mas ainda é lento se o banco está longe do nó edge
import { neon } from "@neondatabase/serverless";
 
export const runtime = "edge";
 
export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  // Isso faz uma requisição HTTP ao seu banco Neon
  // Funciona, mas a latência depende da distância até a região do banco
  const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
  return Response.json(posts);
}

2. Tarefas de Longa Duração#

Edge functions têm limites de tempo de CPU, tipicamente 10-50ms de tempo real de computação. O tempo de relógio é mais generoso (geralmente 30 segundos), mas operações intensivas de CPU vão atingir o limite rápido:

typescript
// ❌ Estes vão exceder limites de tempo de CPU na edge
export const runtime = "edge";
 
export async function POST(request: Request) {
  const data = await request.json();
 
  // Processamento de imagem — CPU intensivo
  // (Também não pode usar sharp porque é um módulo nativo)
  const processed = heavyImageProcessing(data.image);
 
  // Geração de PDF — CPU intensivo + precisa de APIs Node.js
  const pdf = generatePDF(data.content);
 
  // Transformação de dados grande
  const result = data.items // 100.000 itens
    .map(transform)
    .filter(validate)
    .sort(compare)
    .reduce(aggregate, {});
 
  return Response.json(result);
}

Se sua função precisa de mais que alguns milissegundos de tempo de CPU, pertence a um servidor Node.js regional. Ponto final.

3. Dependências Exclusivas do Node.js#

Esta pega as pessoas de surpresa. Um número surpreendente de pacotes npm dependem de módulos built-in do Node.js:

typescript
// ❌ Esses pacotes não vão funcionar na edge
import bcrypt from "bcrypt";            // Binding C++ nativo
import sharp from "sharp";              // Binding C++ nativo
import puppeteer from "puppeteer";      // Precisa de filesystem + child_process
import nodemailer from "nodemailer";    // Precisa do módulo net
import { readFile } from "fs/promises"; // API de filesystem do Node.js
import mongoose from "mongoose";         // Conexões TCP + APIs Node.js
 
// ✅ Alternativas compatíveis com edge
import { hashSync } from "bcryptjs";    // Implementação pura JS (mais lenta)
// Para imagens: use um serviço ou API separados
// Para email: use uma API de email baseada em HTTP (Resend, SendGrid REST)
// Para banco de dados: use clients baseados em HTTP

Antes de mover qualquer coisa para a edge, verifique cada dependência. Um require("fs") enterrado três níveis de profundidade na sua árvore de dependências vai crashar sua edge function em runtime — não em build time. Você faz deploy, tudo parece bem, aí a primeira requisição atinge aquele caminho de código e você recebe um erro críptico.

4. Tamanhos de Bundle Grandes#

Plataformas edge têm limites estritos de tamanho de bundle:

  • Cloudflare Workers: 1MB (gratuito), 5MB (pago)
  • Vercel Edge Functions: 4MB (comprimido)
  • Deno Deploy: 20MB

Isso parece bastante até você importar uma biblioteca de componentes UI, uma biblioteca de validação e uma de datas. Uma vez tive um edge middleware que inchou para 3.5MB porque importei de um barrel file que puxou o diretório @/components inteiro.

typescript
// ❌ Imports de barrel file podem puxar demais
import { validateEmail } from "@/lib/utils";
// Se utils.ts re-exporta de 20 outros módulos, todos são bundleados
 
// ✅ Importe diretamente da fonte
import { validateEmail } from "@/lib/validators/email";

5. Streaming e WebSockets#

Edge functions podem fazer respostas em streaming (Web Streams API), mas conexões WebSocket de longa duração são outra história. Embora algumas plataformas suportem WebSockets na edge (Cloudflare Workers, Deno Deploy), a natureza efêmera das edge functions as torna uma escolha ruim para conexões stateful e de longa duração.

Edge Runtime no Next.js#

O Next.js torna simples optar pelo edge runtime por rota. Você não precisa ir all-in — escolhe exatamente quais rotas rodam na edge.

Middleware (Sempre Edge)#

O middleware do Next.js sempre roda na edge. Isso é por design — o middleware intercepta cada requisição correspondente, então precisa ser rápido e distribuído globalmente:

typescript
// middleware.ts — sempre roda na edge, sem necessidade de opt-in
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Isso roda antes de cada requisição correspondente
  // Mantenha rápido — sem chamadas de banco, sem computação pesada
  return NextResponse.next();
}
 
export const config = {
  // Rodar apenas em caminhos específicos
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
  ],
};

Rotas de API na Edge#

Qualquer route handler pode optar pelo edge runtime:

typescript
// app/api/hello/route.ts
export const runtime = "edge"; // Esta única linha muda o runtime
 
export async function GET(request: Request) {
  return Response.json({
    message: "Hello from the edge",
    region: process.env.VERCEL_REGION ?? "unknown",
    timestamp: Date.now(),
  });
}

Rotas de Página na Edge#

Até páginas inteiras podem renderizar na edge, embora eu pensaria cuidadosamente antes de fazer isso:

typescript
// app/dashboard/page.tsx
export const runtime = "edge";
 
export default async function DashboardPage() {
  // Lembre: sem APIs Node.js aqui
  // Qualquer busca de dados deve usar fetch() ou clients compatíveis com edge
  const data = await fetch("https://api.example.com/dashboard", {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
    next: { revalidate: 60 },
  }).then((r) => r.json());
 
  return (
    <main>
      <h1>Dashboard</h1>
      {/* renderizar dados */}
    </main>
  );
}

O Que Está Disponível no Edge Runtime#

Aqui está uma referência prática do que você pode e não pode usar:

typescript
// ✅ Disponível na edge
fetch()                          // Requisições HTTP
Request / Response               // Request/response padrão web
Headers                          // Headers HTTP
URL / URLSearchParams            // Parse de URL
TextEncoder / TextDecoder        // Codificação de strings
crypto.subtle                    // Operações crypto (assinatura, hash)
crypto.randomUUID()              // Geração de UUID
crypto.getRandomValues()         // Números aleatórios criptográficos
structuredClone()                // Clonagem profunda
atob() / btoa()                  // Codificação/decodificação Base64
setTimeout() / setInterval()     // Timers (mas lembre dos limites de CPU)
console.log()                    // Logging
ReadableStream / WritableStream  // Streaming
AbortController / AbortSignal    // Cancelamento de requisições
URLPattern                       // Pattern matching de URL
 
// ❌ NÃO disponível na edge
require()                        // CommonJS (use import)
fs / path / os                   // Módulos built-in do Node.js
process.exit()                   // Controle de processo
Buffer                           // Use Uint8Array em vez
__dirname / __filename           // Use import.meta.url
setImmediate()                   // Não é padrão web

Auth na Edge: O Padrão Completo#

Quero aprofundar em autenticação porque é um dos casos de uso de edge mais impactantes, mas também é fácil errar.

O padrão que funciona é: verificar o token na edge, passar claims confiáveis downstream, nunca tocar no banco de dados no middleware.

typescript
// lib/edge-auth.ts — Utilitários de auth compatíveis com edge
import { jwtVerify, SignJWT, importSPKI, importPKCS8 } from "jose";
 
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY!;
const ISSUER = "https://auth.myapp.com";
const AUDIENCE = "https://myapp.com";
 
export interface TokenPayload {
  sub: string;
  email: string;
  role: "user" | "admin" | "moderator";
  iat: number;
  exp: number;
}
 
export async function verifyToken(token: string): Promise<TokenPayload | null> {
  try {
    const publicKey = await importSPKI(PUBLIC_KEY_PEM, "RS256");
    const { payload } = await jwtVerify(token, publicKey, {
      algorithms: ["RS256"],
      issuer: ISSUER,
      audience: AUDIENCE,
      clockTolerance: 30, // 30 segundos de tolerância de desvio de relógio
    });
 
    return payload as unknown as TokenPayload;
  } catch {
    return null;
  }
}
 
export function isTokenExpiringSoon(payload: TokenPayload): boolean {
  const now = Math.floor(Date.now() / 1000);
  const fiveMinutes = 5 * 60;
  return payload.exp - now < fiveMinutes;
}
typescript
// middleware.ts — O middleware de auth
import { NextRequest, NextResponse } from "next/server";
import { verifyToken, isTokenExpiringSoon } from "./lib/edge-auth";
 
const PUBLIC_PATHS = ["/", "/login", "/register", "/api/auth/login"];
const ADMIN_PATHS = ["/admin"];
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // Pular auth para caminhos públicos
  if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
    return NextResponse.next();
  }
 
  // Extrair token
  const token = request.cookies.get("auth-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  // Verificar token (pura criptografia — sem chamada ao banco)
  const payload = await verifyToken(token);
  if (!payload) {
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("auth-token");
    return response;
  }
 
  // Controle de acesso baseado em papel
  if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }
 
  // Passar informações verificadas do usuário para a origem como headers confiáveis
  const response = NextResponse.next();
  response.headers.set("x-user-id", payload.sub);
  response.headers.set("x-user-email", payload.email);
  response.headers.set("x-user-role", payload.role);
 
  // Sinalizar se o token precisa de refresh
  if (isTokenExpiringSoon(payload)) {
    response.headers.set("x-token-refresh", "true");
  }
 
  return response;
}
typescript
// app/api/profile/route.ts — Servidor de origem lê headers confiáveis
export async function GET(request: Request) {
  // Esses headers foram definidos pelo edge middleware após verificação JWT
  // São confiáveis porque vêm da nossa própria infraestrutura
  const userId = request.headers.get("x-user-id");
  const userRole = request.headers.get("x-user-role");
 
  if (!userId) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // Agora podemos acessar o banco — estamos no servidor de origem,
  // bem perto do banco, com um pool de conexões
  const user = await db.user.findUnique({ where: { id: userId } });
 
  return Response.json(user);
}

A percepção chave: a edge faz a parte rápida (verificação criptográfica), e a origem faz a parte lenta (queries de banco). Cada uma roda onde é mais eficiente.

Uma ressalva importante: isso só funciona para JWTs. Se seu sistema de auth requer um lookup no banco a cada requisição (como auth baseada em sessão com cookie de session ID), a edge não pode ajudar — você ainda precisaria chamar o banco, o que significa uma ida e volta até a região de origem.

Cache na Edge#

Cache na edge é onde as coisas ficam interessantes. Nós edge podem fazer cache de respostas, o que significa que requisições subsequentes para a mesma URL são servidas diretamente da edge sem atingir sua origem.

Cache-Control Feito Certo#

typescript
// app/api/products/route.ts
export const runtime = "edge";
 
export async function GET(request: Request) {
  const url = new URL(request.url);
  const category = url.searchParams.get("category") ?? "all";
 
  const products = await fetch(
    `${process.env.ORIGIN_API}/products?category=${category}`,
  ).then((r) => r.json());
 
  return Response.json(products, {
    headers: {
      // Cache na CDN por 60 segundos
      // Servir stale enquanto revalida por até 5 minutos
      // Cliente pode cachear por 10 segundos
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
 
      // Variar por esses headers para que variantes diferentes tenham entradas de cache diferentes
      Vary: "Accept-Language, Accept-Encoding",
 
      // Tag de cache específica da CDN para invalidação direcionada
      "Cache-Tag": `products,category-${category}`,
    },
  });
}

O padrão stale-while-revalidate é particularmente poderoso na edge. Veja o que acontece:

  1. Primeira requisição: Edge busca da origem, faz cache da resposta, retorna
  2. Requisições dentro de 60 segundos: Edge serve do cache (0ms de latência de origem)
  3. Requisição em 61-360 segundos: Edge serve a versão stale cacheada imediatamente, mas busca uma versão fresca da origem em background
  4. Após 360 segundos: Cache totalmente expirado, próxima requisição vai para a origem

Seus usuários quase sempre recebem uma resposta cacheada. O tradeoff de freshness é explícito e ajustável.

Edge Config para Configuração Dinâmica#

O Edge Config da Vercel (e serviços similares de outras plataformas) permite armazenar configuração key-value que é replicada para cada localização edge. Isso é incrivelmente útil para feature flags, regras de redirecionamento e configuração de testes A/B que você quer atualizar sem redeployar:

typescript
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
 
export async function middleware(request: NextRequest) {
  // Leituras do Edge Config são extremamente rápidas (~1ms) porque
  // os dados são replicados para cada localização edge
  const maintenanceMode = await get<boolean>("maintenance_mode");
 
  if (maintenanceMode) {
    return NextResponse.rewrite(new URL("/maintenance", request.url));
  }
 
  // Feature flags
  const features = await get<Record<string, boolean>>("feature_flags");
  if (features?.["new_pricing_page"] && request.nextUrl.pathname === "/pricing") {
    return NextResponse.rewrite(new URL("/pricing-v2", request.url));
  }
 
  // Redirecionamentos dinâmicos (atualizar redirecionamentos sem redeploy)
  const redirects = await get<Array<{ from: string; to: string; permanent: boolean }>>(
    "redirects",
  );
 
  if (redirects) {
    const match = redirects.find((r) => r.from === request.nextUrl.pathname);
    if (match) {
      return NextResponse.redirect(
        new URL(match.to, request.url),
        match.permanent ? 308 : 307,
      );
    }
  }
 
  return NextResponse.next();
}

Isso é genuinamente revolucionário. Antes do Edge Config, mudar uma feature flag significava mudança de código e redeploy. Agora você atualiza um valor JSON em um dashboard e ele se propaga globalmente em segundos.

A Matemática Real de Performance#

Vamos fazer a matemática honesta em vez da matemática de marketing. Vou comparar três arquiteturas para um endpoint de API típico que precisa consultar um banco de dados:

Cenário: API de Perfil de Usuário (2 queries de banco)#

Arquitetura A: Node.js Regional Tradicional

Usuário em Tóquio → Origem na Virginia: 140ms
  + Query BD 1 (mesma região): 2ms
  + Query BD 2 (mesma região): 2ms
  + Processamento: 5ms
  = Total: ~149ms

Arquitetura B: Edge Function com Banco HTTP

Usuário em Tóquio → Edge em Tóquio: 5ms
  + Query BD 1 (HTTP para Virginia): 145ms
  + Query BD 2 (HTTP para Virginia): 145ms
  + Processamento: 3ms
  = Total: ~298ms ← MAIS LENTO que regional

Arquitetura C: Edge Function com Banco Regional (réplica de leitura)

Usuário em Tóquio → Edge em Tóquio: 5ms
  + Query BD 1 (HTTP para réplica em Tóquio): 8ms
  + Query BD 2 (HTTP para réplica em Tóquio): 8ms
  + Processamento: 3ms
  = Total: ~24ms ← Mais rápido, mas requer BD multi-região

Arquitetura D: Edge para Auth + Regional para Dados

Usuário em Tóquio → Edge middleware em Tóquio: 5ms (verificação JWT)
  → Origem na Virginia: 140ms
  + Query BD 1 (mesma região): 2ms
  + Query BD 2 (mesma região): 2ms
  + Processamento: 5ms
  = Total: ~154ms
  (Mas auth já está verificada — origem não precisa re-verificar)
  (E requisições não autorizadas são bloqueadas na edge — nunca alcançam a origem)

As conclusões:

  1. Edge + banco na origem = frequentemente mais lento que apenas usar um servidor regional
  2. Edge + banco multi-região = mais rápido mas mais caro e complexo
  3. Edge para gatekeeping + regional para dados = melhor equilíbrio pragmático
  4. Edge pura (sem banco) = imbatível para coisas como redirecionamentos e verificações de auth

A Arquitetura D é o que uso para a maioria dos projetos. A edge lida com o que faz bem (decisões rápidas, auth, roteamento), e o servidor Node.js regional lida com o que faz bem (queries de banco, computação pesada).

Quando a Edge Realmente Ganha: Operações Sem Banco de Dados#

A matemática muda completamente quando não há banco de dados envolvido:

Redirecionamento (edge):
  Usuário em Tóquio → Edge em Tóquio → resposta de redirect: ~5ms

Redirecionamento (regional):
  Usuário em Tóquio → Origem na Virginia → resposta de redirect: ~280ms
Resposta de API estática (edge + cache):
  Usuário em Tóquio → Edge em Tóquio → resposta cacheada: ~5ms

Resposta de API estática (regional):
  Usuário em Tóquio → Origem na Virginia → resposta: ~280ms
Bloqueio de bot (edge):
  Bot malicioso em qualquer lugar → Edge (mais próximo) → resposta 403: ~5ms
  (Bot nunca alcança seu servidor de origem)

Bloqueio de bot (regional):
  Bot malicioso em qualquer lugar → Origem na Virginia → resposta 403: ~280ms
  (Bot ainda consumiu recursos da origem)

Para operações que não precisam de banco de dados, a edge é 20-50x mais rápida. Isso não é marketing — é física.

Meu Framework de Decisão#

Após um ano trabalhando com edge functions em produção, aqui está o fluxograma que uso para cada novo endpoint ou trecho de lógica:

Passo 1: Precisa de APIs do Node.js?#

Se importa fs, net, child_process, ou qualquer módulo nativo — Node.js regional. Sem debate.

Passo 2: Precisa de queries de banco?#

Se sim, e você não tem réplicas de leitura perto dos seus usuários — Node.js regional (na mesma região do seu banco). As idas e voltas ao banco vão dominar.

Se sim, e você tem réplicas de leitura distribuídas globalmente — Edge pode funcionar, usando clients de banco baseados em HTTP.

Passo 3: É uma decisão sobre uma requisição (roteamento, auth, redirect)?#

Se sim — Edge. Este é o ponto ideal. Você está tomando uma decisão rápida que determina o que acontece com a requisição antes de alcançar a origem.

Passo 4: A resposta é cacheável?#

Se sim — Edge com headers Cache-Control adequados. Mesmo que a primeira requisição vá para sua origem, requisições subsequentes servem do cache da edge.

Passo 5: É CPU-intensivo?#

Se envolve computação significativa (processamento de imagem, geração de PDF, transformações de dados grandes) — Node.js regional.

Passo 6: Quão sensível à latência é?#

Se é um job em background ou webhook — Node.js regional. Ninguém está esperando por ele. Se é uma requisição voltada ao usuário onde cada ms importa — Edge, se atende os outros critérios.

A Cola#

typescript
// ✅ PERFEITO para edge
// - Middleware (auth, redirects, rewrites, headers)
// - Lógica de geolocalização
// - Atribuição de teste A/B
// - Detecção de bots / regras WAF
// - Respostas de API amigáveis a cache
// - Verificações de feature flags
// - Respostas CORS preflight
// - Transformações de dados estáticos (sem BD)
// - Verificação de assinatura de webhook
 
// ❌ MANTENHA no Node.js regional
// - Operações CRUD de banco de dados
// - Uploads / processamento de arquivos
// - Manipulação de imagens
// - Geração de PDF
// - Envio de email (use API HTTP, mas ainda regional)
// - Servidores WebSocket
// - Jobs em background / filas
// - Qualquer coisa usando pacotes npm nativos
// - Páginas SSR com queries de banco
// - Resolvers GraphQL que acessam bancos
 
// 🤔 DEPENDE
// - Autenticação (edge para JWT, regional para sessão-BD)
// - Rotas de API (edge se sem BD, regional se BD)
// - Páginas renderizadas no servidor (edge se dados vêm de cache/fetch, regional se BD)
// - Features em tempo real (edge para auth inicial, regional para conexões persistentes)

O Que Eu Realmente Rodo na Edge#

Para este site, aqui está a divisão:

Edge (middleware):

  • Detecção de locale e redirecionamento
  • Filtragem de bots
  • Headers de segurança (CSP, HSTS, etc.)
  • Log de acesso
  • Rate limiting (básico)

Node.js regional:

  • Renderização de conteúdo do blog (processamento MDX precisa de APIs Node.js via Velite)
  • Rotas de API que acessam Redis
  • Geração de imagem OG (precisa de mais tempo de CPU)
  • Geração de feed RSS

Estático (sem runtime):

  • Páginas de ferramentas (pré-renderizadas em build time)
  • Páginas de posts do blog (pré-renderizadas em build time)
  • Todas as imagens e assets (servidos por CDN)

O melhor runtime é frequentemente nenhum runtime. Se você pode pré-renderizar algo em build time e servir como asset estático, isso sempre será mais rápido que qualquer edge function. A edge é para as coisas que genuinamente precisam ser dinâmicas a cada requisição.

O Resumo Honesto#

Edge functions não são um substituto para servidores tradicionais. São um complemento. São uma ferramenta adicional na sua caixa de ferramentas de arquitetura — uma que é incrivelmente poderosa para os casos de uso certos e ativamente prejudicial para os errados.

A heurística que sempre volto: se sua função precisa acessar um banco de dados em uma única região, colocar a função na edge não ajuda — prejudica. Você acabou de adicionar um salto. A função roda mais rápido, mas depois gasta 100ms+ alcançando de volta o banco. Resultado líquido: mais lento que rodar tudo em uma região.

Mas para decisões que podem ser feitas apenas com as informações na própria requisição — geolocalização, cookies, headers, JWTs — a edge é imbatível. Essas respostas de 5ms da edge não são benchmarks sintéticos. São reais, e seus usuários sentem a diferença.

Não mova tudo para a edge. Não mantenha tudo fora da edge. Coloque cada parte da lógica onde a física a favorece.

Posts Relacionados