Edge Functions vs Serverless: O Que Muda na Prática

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O cold start que ninguém menciona no pitch de vendas
Uma Serverless Function na AWS Lambda (runtime Node.js 20) com um bundle de 5 MB leva entre 300 ms e 800 ms no primeiro cold start, dependendo da região e da memória alocada. Uma Edge Function na Cloudflare Workers ou no Vercel Edge Runtime inicia em menos de 10 ms. Essa diferença parece encerrar a discussão: edge ganha. Só que a história não termina na latência de boot.
Edge Functions rodam em um runtime restrito. Sem fs, sem net, sem child_process, sem acesso a TCP arbitrário. Serverless Functions rodam um Node.js completo (ou Python, Go, Java, Rust). A escolha entre as duas não é sobre qual é "melhor": é sobre qual conjunto de restrições você aceita em troca de qual benefício.
O que cada uma é, sem marketing
Serverless Functions executam em containers efêmeros em data centers específicos (us-east-1, sa-east-1, eu-west-1). O provedor gerencia o scaling, mas a função roda em uma região fixa. O runtime é completo: Node.js com acesso a todas as APIs nativas, incluindo binários compilados via layers.
Edge Functions executam em pontos de presença (PoPs) distribuídos globalmente, usando runtimes leves baseados em V8 isolates (Cloudflare Workers, Deno Deploy) ou versões restritas do Node.js (Vercel Edge Runtime). O código roda perto do usuário, mas dentro de um sandbox com limites rígidos de CPU, memória e APIs disponíveis.
| Critério | Serverless (Lambda/Cloud Functions) | Edge Functions (Workers/Vercel Edge) |
|---|---|---|
| Runtime | Node.js, Python, Go, Java, Rust completos | V8 isolates ou Node.js restrito |
| Cold start típico | 300-800 ms (Node.js), 1-5 s (Java) | < 10 ms |
| Tempo máximo de execução | 15 min (Lambda), 60 min (Cloud Run) | 30 s (Cloudflare), 25 s (Vercel Edge) |
| Memória máxima | 10 GB (Lambda) | 128 MB (Workers), 1 GB (Vercel) |
| APIs Node.js disponíveis | Todas | Subconjunto (sem fs, net, child_process, dgram) |
| Localização | Região fixa | PoPs globais (300+ locais) |
| Acesso a banco relacional | Direto via TCP | Via HTTP/WebSocket (Neon, PlanetScale, Supabase) |
| Preço por invocação (referência) | ~$0.20/1M (Lambda) | ~$0.50/1M (Workers paid) |
Quando edge resolve e quando atrapalha
Edge Functions brilham em lógica leve que precisa rodar perto do usuário. Três cenários onde a escolha é clara:
Rewrite e redirect com lógica: verificar um cookie de A/B test e redirecionar antes de o request chegar ao origin server. Isso é o que o Next.js Middleware faz na edge.
// middleware.ts (Vercel Edge Runtime / Next.js Middleware)
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const bucket = request.cookies.get("ab-bucket")?.value;
if (!bucket) {
// Atribui bucket no primeiro acesso para manter consistência durante a sessão
const assigned = Math.random() > 0.5 ? "control" : "variant";
const response = NextResponse.next();
response.cookies.set("ab-bucket", assigned, { maxAge: 60 * 60 * 24 * 30 });
return response;
}
if (bucket === "variant") {
return NextResponse.rewrite(new URL("/landing-variant", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/landing"],
};Autenticação e autorização antes do origin: validar um JWT e rejeitar requests inválidos antes de consumir compute no backend.
// Cloudflare Worker: validação de JWT na edge
// Usa Web Crypto API (disponível em V8 isolates, sem dependência de node:crypto)
export default {
async fetch(request: Request): Promise<Response> {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return new Response("Unauthorized", { status: 401 });
}
const token = authHeader.slice(7);
try {
const payload = await verifyJwt(token);
// Propaga identidade para o origin via header customizado
const upstreamRequest = new Request(request);
upstreamRequest.headers.set("X-User-Id", payload.sub);
return fetch(upstreamRequest);
} catch {
return new Response("Invalid token", { status: 403 });
}
},
};
async function verifyJwt(token: string): Promise<{ sub: string }> {
const [headerB64, payloadB64, signatureB64] = token.split(".");
const encoder = new TextEncoder();
// Importa a chave pública como CryptoKey (RS256)
const key = await crypto.subtle.importKey(
"jwk",
JWT_PUBLIC_JWK, // Variável de ambiente ou KV binding
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
false,
["verify"]
);
const data = encoder.encode(`${headerB64}.${payloadB64}`);
const signature = base64UrlDecode(signatureB64);
const valid = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data);
if (!valid) throw new Error("Signature mismatch");
return JSON.parse(atob(payloadB64.replace(/-/g, "+").replace(/_/g, "/")));
}
function base64UrlDecode(str: string): ArrayBuffer {
const binary = atob(str.replace(/-/g, "+").replace(/_/g, "/"));
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes.buffer;
}Geolocalização e personalização: servir conteúdo diferente com base no país do request sem round-trip ao backend.
// Vercel Edge Function com geolocalização
import { NextRequest, NextResponse } from "next/server";
export const config = { runtime: "edge" };
export default function handler(request: NextRequest) {
const country = request.geo?.country ?? "US";
// Retorna pricing na moeda local sem consultar banco
const pricing: Record<string, { currency: string; price: number }> = {
BR: { currency: "BRL", price: 49.9 },
US: { currency: "USD", price: 9.99 },
PT: { currency: "EUR", price: 8.99 },
};
const plan = pricing[country] ?? pricing["US"];
return NextResponse.json({ country, ...plan });
}Esses três cenários compartilham uma característica: lógica leve, sem dependência de I/O pesado ou APIs Node.js nativas. Quando a lógica precisa de mais, serverless tradicional é o caminho.
Onde serverless tradicional continua necessário
Se a sua função precisa de qualquer um destes itens, edge não serve:
-
Conexão TCP direta com banco relacional: PostgreSQL, MySQL e SQL Server usam protocolos binários sobre TCP. Edge Functions não têm acesso a
net. Serviços como Neon e PlanetScale oferecem drivers HTTP, mas com overhead de serialização e latência adicional por request. Para queries complexas com transações, uma Lambda emsa-east-1conectando via TCP ao RDS na mesma VPC é mais rápida que uma Edge Function em São Paulo fazendo HTTP para um banco em Virginia. -
Processamento de arquivos: upload de imagens, geração de PDF, manipulação de CSV. Sem
fs, semchild_process, sem sharp, sem ffmpeg. -
Dependências nativas (N-API/WASM pesado): Prisma Client com query engine binária, bcrypt nativo, bibliotecas que compilam C/C++.
-
Execução longa: jobs que levam mais de 30 segundos. Para isso, serverless com timeout estendido ou processamento assíncrono com filas é a solução.
// Lambda function: processamento que exige runtime Node.js completo
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import sharp from "sharp";
import { Readable } from "node:stream";
const s3 = new S3Client({ region: "sa-east-1" });
export async function handler(event: { bucket: string; key: string }) {
const { Body } = await s3.send(
new GetObjectCommand({ Bucket: event.bucket, Key: event.key })
);
// sharp usa libvips (binário nativo): impossível em Edge Functions
const optimized = await sharp(await streamToBuffer(Body as Readable))
.resize(800, 600, { fit: "inside", withoutEnlargement: true })
.webp({ quality: 80 })
.toBuffer();
const outputKey = event.key.replace(/\.\w+$/, ".webp");
await s3.send(
new PutObjectCommand({
Bucket: event.bucket,
Key: outputKey,
Body: optimized,
ContentType: "image/webp",
})
);
return { statusCode: 200, outputKey };
}
async function streamToBuffer(stream: Readable): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks);
}O que NÃO fazer
Anti-pattern 1: Colocar query pesada na edge
O erro mais comum é mover toda a API para Edge Functions achando que "mais perto do usuário = mais rápido". Se a edge function faz uma query HTTP para um banco em us-east-1, o request viaja do PoP em São Paulo até Virginia, executa a query, e volta. O ganho de latência no boot (10 ms vs 500 ms) é consumido pelo round-trip intercontinental (150-200 ms cada direção).
// ERRADO: Edge Function fazendo query pesada em banco remoto
export const config = { runtime: "edge" };
export default async function handler() {
// Neon serverless driver via HTTP: cada query é um round-trip HTTP
const response = await fetch("https://your-project.us-east-1.aws.neon.tech/sql", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.NEON_API_KEY}` },
body: JSON.stringify({
// Join complexo com agregação: ~50-200 ms de execução no banco
// + ~300 ms de round-trip São Paulo -> Virginia -> São Paulo
query: `
SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.total) as lifetime_value
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name
ORDER BY lifetime_value DESC
LIMIT 50
`,
}),
});
return new Response(await response.text(), {
headers: { "Content-Type": "application/json" },
});
}// CORRETO: Serverless Function na mesma região do banco
// Lambda em us-east-1, RDS/Neon em us-east-1: latência de rede < 5 ms
import { Pool } from "pg";
// Pool reutilizado entre invocações quentes (Lambda reusa o container)
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 1, // Uma conexão por instância Lambda para evitar exaustão do pool
});
export async function handler() {
const { rows } = await pool.query(`
SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.total) as lifetime_value
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id, u.name
ORDER BY lifetime_value DESC
LIMIT 50
`);
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(rows),
};
}Anti-pattern 2: Importar SDK pesado na edge
// ERRADO: importar aws-sdk completo em Edge Function
// O bundle ultrapassa o limite de 1 MB (Workers) ou 4 MB (Vercel Edge)
import AWS from "aws-sdk"; // ~70 MB descomprimido, tree-shaking limitado
export const config = { runtime: "edge" };// CORRETO: se precisa de AWS SDK, use Serverless Function com runtime Node.js
// Ou use apenas o client modular (@aws-sdk/client-s3 isolado) se o bundle couber
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
// Client modular: ~3 MB, cabe em Lambda, não cabe em Workers (1 MB limit)Arquitetura híbrida: edge como gateway, serverless como backend
A arquitetura que extrai o melhor dos dois mundos usa edge para decisões rápidas e serverless para lógica pesada. É o mesmo princípio de um API Gateway, só que distribuído.
Usuário (São Paulo)
│
▼
Edge Function (PoP São Paulo) ─── 1. Valida JWT (~2 ms)
│ 2. Rate limiting via KV (~5 ms)
│ 3. Redireciona A/B test
│
▼ (se autorizado)
Serverless Function (sa-east-1) ── 4. Query no banco (~10 ms rede + query)
│ 5. Lógica de negócio
│ 6. Resposta
▼
Usuário (São Paulo)No Next.js, essa separação acontece naturalmente: o Middleware roda na edge, as Route Handlers e Server Components podem rodar em Node.js ou edge, e as Server Actions rodam no runtime que você especificar.
// next.config.ts: controle granular de runtime por rota
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Middleware SEMPRE roda na edge (não configurável)
// Route Handlers e pages: configuráveis por arquivo
};
export default nextConfig;// app/api/lightweight/route.ts: rota leve na edge
export const runtime = "edge";
export async function GET(request: Request) {
// Lógica que não precisa de Node.js APIs
const data = { timestamp: Date.now(), region: "auto" };
return Response.json(data);
}// app/api/heavy/route.ts: rota pesada no Node.js
export const runtime = "nodejs"; // Padrão no App Router, mas explícito é melhor
import { prisma } from "@/lib/prisma";
export async function GET() {
// Prisma usa query engine binária: precisa de Node.js runtime
const users = await prisma.user.findMany({
include: { orders: { where: { status: "COMPLETED" } } },
take: 50,
});
return Response.json(users);
}Essa separação é o que faz projetos como o Supabase funcionar bem com Next.js: o Supabase client usa HTTP (funciona na edge), enquanto operações que exigem service role key ou lógica pesada ficam em Server Components com runtime Node.js.
Matriz de decisão
Use esta matriz para decidir onde colocar cada pedaço de lógica:
| Tipo de lógica | Runtime recomendado | Por quê |
|---|---|---|
| Autenticação/autorização (JWT, session check) | Edge | Rejeita requests inválidos antes do origin |
| A/B testing, feature flags | Edge | Decisão por cookie/header, sem I/O pesado |
| Geolocalização, personalização por país | Edge | Dados disponíveis no request (headers do CDN) |
| Rate limiting simples (por IP/token) | Edge | Leitura/escrita em KV distribuído |
| CRUD com banco relacional | Serverless (mesma região do banco) | Latência de rede domina; TCP direto é mais eficiente |
| Upload e processamento de arquivo | Serverless | Precisa de fs, binários nativos, memória |
| Webhook processing | Serverless | Pode exigir execução longa e retries |
| Geração de PDF/imagem | Serverless | Dependências nativas (Puppeteer, sharp) |
| Cache invalidation, revalidação ISR | Edge | Lógica leve que dispara revalidação |
Para quem está estruturando o backend completo, a decisão de runtime se conecta diretamente com a arquitetura de comunicação entre serviços e com a estratégia de deploy.
FAQ
Edge Functions substituem Serverless Functions?
Não. Elas cobrem cenários diferentes. Edge Functions são um complemento para lógica leve e latency-sensitive. Qualquer operação que precise de runtime Node.js completo, conexão TCP direta, binários nativos ou execução acima de 30 segundos continua em Serverless Functions.
Posso usar Prisma em Edge Functions?
O Prisma oferece um "edge client" experimental que usa o Prisma Accelerate (proxy HTTP) ou drivers de banco compatíveis com edge (como o driver HTTP do Neon). Funciona, mas cada query passa por uma camada HTTP adicional. Para aplicações com queries simples e baixo volume, é aceitável. Para queries complexas com transações, a latência extra e a falta de connection pooling TCP tornam o setup inferior a uma Lambda na mesma região do banco.
O custo de Edge Functions é maior?
Depende do padrão de uso. Cloudflare Workers cobra ~$0.50/1M requests no plano pago (com 10M inclusos por $5/mês). Lambda cobra ~$0.20/1M requests mais o custo de compute por GB-segundo. Para funções que executam rápido (< 50 ms), edge tende a ser mais barato. Para funções que consomem mais CPU e memória, Lambda com ARM (Graviton) costuma vencer no custo.
Como testar Edge Functions localmente?
Cloudflare Workers usa o wrangler dev, que emula o runtime V8 isolate localmente. Vercel Edge Functions podem ser testadas com next dev (o Next.js simula o edge runtime). Para testes de integração, o miniflare oferece um ambiente local completo com KV, D1 e R2 simulados.
Edge Functions funcionam bem para APIs que servem o Brasil?
Se o banco está em sa-east-1 e a edge function roda no PoP de São Paulo, o round-trip ao banco via HTTP adiciona latência comparável a uma Lambda na mesma região conectando via TCP. O ganho real da edge aparece quando a resposta não depende de banco (cache, redirect, auth) ou quando o público é global e você quer latência baixa em todos os continentes.
A decisão é sobre onde fica o dado, não onde roda o código
A pergunta "edge ou serverless?" está incompleta sem saber onde mora o banco de dados. Se 90% dos seus usuários estão no Brasil e o banco está em São Paulo, uma Lambda em sa-east-1 com cold start de 500 ms atende melhor que uma Edge Function em São Paulo fazendo HTTP para um banco em Virginia. A latência que importa não é a de boot da função: é a de ida e volta até o dado.
Use edge para o que não precisa de dado persistente ou para o que pode ser resolvido com um KV distribuído. Use serverless para o que precisa de banco relacional, processamento pesado ou runtime completo. Misture os dois quando a arquitetura justificar. A pior decisão é escolher por hype e descobrir as limitações em produção.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando

Deno + Deno KV + Deno Deploy: API Serverless com Banco de Dados Embutido

Cloudflare Workers + CLI: Deploy de APIs JavaScript na Edge sem Docker, sem Kubernetes

API Gateway Patterns: Autenticação, Rate Limiting e Caching com Node.js
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.