Zum Inhalt springen
·17 Min. Lesezeit

Edge Functions: Was sie sind, wann du sie nutzen solltest und wann nicht

Edge Runtime, V8 Isolates, der Cold-Start-Mythos, Geo-Routing, A/B-Testing, Auth an der Edge und warum ich einiges zurück zu Node.js verschoben habe. Ein ausgewogener Blick auf Edge Computing.

Teilen:X / TwitterLinkedIn

Das Wort „Edge" wird ständig in den Raum geworfen. Vercel sagt es. Cloudflare sagt es. Deno sagt es. Jeder Konferenz-Vortrag über Web-Performance erwähnt unweigerlich „Ausführung an der Edge", als wäre es eine magische Beschwörung, die deine App schnell macht.

Ich bin darauf angesprungen. Ich habe Middleware, API-Routen und sogar etwas Rendering-Logik auf die Edge Runtime verlagert. Einige dieser Entscheidungen waren brillant. Andere habe ich drei Wochen später stillschweigend zurück zu Node.js verschoben, nachdem ich um 2 Uhr nachts Connection-Pool-Fehler debuggt hatte.

Dieser Beitrag ist die ausgewogene Version dieser Geschichte — was die Edge tatsächlich ist, wo sie wirklich glänzt, wo sie absolut versagt, und wie ich für jedes Stück meiner Anwendung entscheide, welche Runtime ich verwende.

Was ist die Edge?#

Beginnen wir mit der Geografie. Wenn jemand deine Website besucht, reist die Anfrage vom Gerät über den Internetanbieter, durchs Internet zu deinem Server, wird dort verarbeitet, und die Antwort reist den ganzen Weg zurück. Wenn dein Server in us-east-1 (Virginia) steht und dein Nutzer in Tokio ist, legt diese Rundreise ungefähr 14.000 km zurück. Bei Lichtgeschwindigkeit durch Glasfaser sind das etwa 70 ms nur für die Physik — in eine Richtung. Rechne DNS-Auflösung, TLS-Handshake und Verarbeitungszeit dazu, und du landest leicht bei 200–400 ms, bevor dein Nutzer ein einziges Byte sieht.

„Edge" bedeutet, deinen Code auf weltweit verteilten Servern auszuführen — denselben CDN-Knoten, die schon immer statische Assets ausgeliefert haben, aber jetzt können sie auch deine Logik ausführen. Statt eines einzigen Origin-Servers in Virginia läuft dein Code an über 300 Standorten weltweit. Ein Nutzer in Tokio trifft auf einen Server in Tokio. Ein Nutzer in Paris trifft auf einen Server in Paris.

Die Latenzmathematik ist einfach und überzeugend:

Traditionell (einzelner Origin):
  Tokio → Virginia: ~140ms Round Trip (allein die Physik)
  + TLS-Handshake: ~140ms mehr (ein weiterer Round Trip)
  + Verarbeitung: 20-50ms
  Gesamt: ~300-330ms

Edge (lokaler PoP):
  Tokio → Tokio Edge-Knoten: ~5ms Round Trip
  + TLS-Handshake: ~5ms mehr
  + Verarbeitung: 5-20ms
  Gesamt: ~15-30ms

Das ist eine 10–20-fache Verbesserung bei der initialen Antwort. Es ist real, es ist messbar, und für bestimmte Operationen ist es transformativ.

Aber hier ist, was das Marketing gerne überspringt: Die Edge ist keine vollwertige Serverumgebung. Sie ist etwas grundlegend anderes.

V8 Isolates vs. Node.js#

Traditionelles Node.js läuft in einem vollständigen Betriebssystem-Prozess. Es hat Zugriff auf das Dateisystem, kann TCP-Verbindungen öffnen, Kindprozesse starten, Umgebungsvariablen als Stream lesen — es kann im Grunde alles, was ein Linux-Prozess kann.

Edge Functions laufen nicht auf Node.js. Sie laufen auf V8 Isolates — derselben JavaScript-Engine, die Chrome antreibt, aber auf ihren Kern reduziert. Stell dir ein V8 Isolate als leichtgewichtige Sandbox vor:

typescript
// Das funktioniert in Node.js, aber NICHT an der Edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
 
const file = fs.readFileSync("/etc/hosts");        // ❌ Kein Dateisystem
const conn = createConnection({ port: 5432 });     // ❌ Kein Raw TCP
const result = execSync("ls -la");                  // ❌ Keine Kindprozesse
process.env.DATABASE_URL;                           // ⚠️  Verfügbar, aber statisch, zur Deploy-Zeit gesetzt

Was du an der Edge HAST, ist die Web-API-Oberfläche — dieselben APIs, die auch im Browser verfügbar sind:

typescript
// All das funktioniert an der 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());

Die Einschränkungen sind real und hart:

  • Arbeitsspeicher: 128 MB pro Isolate (Cloudflare Workers), 256 MB auf einigen Plattformen
  • CPU-Zeit: 10–50 ms tatsächliche CPU-Zeit (nicht Wanduhrzeit — await fetch() zählt nicht, aber JSON.parse() auf einem 5-MB-Payload schon)
  • Keine nativen Module: Alles, was ein C++-Binding braucht (bcrypt, sharp, canvas), fällt weg
  • Keine persistenten Verbindungen: Du kannst keine Datenbankverbindung zwischen Requests offenhalten
  • Beschränkte Bundlegröße: Typischerweise 1–5 MB für das gesamte Worker-Script

Das ist nicht Node.js auf einem CDN. Es ist eine andere Runtime mit einem anderen mentalen Modell.

Cold Starts: Mythos und Realität#

Du hast wahrscheinlich gehört, dass Edge Functions „null Cold Starts" haben. Das ist... größtenteils wahr, und der Vergleich ist tatsächlich dramatisch.

Eine traditionelle Container-basierte Serverless-Funktion (AWS Lambda, Google Cloud Functions) funktioniert so:

  1. Anfrage kommt an
  2. Plattform stellt einen Container bereit (falls keiner verfügbar)
  3. Container bootet das Betriebssystem
  4. Runtime wird initialisiert (Node.js, Python usw.)
  5. Dein Code wird geladen und initialisiert
  6. Anfrage wird verarbeitet

Die Schritte 2–5 sind der Cold Start. Für eine Node.js-Lambda sind das typischerweise 200–500 ms. Für eine Java-Lambda können es 2–5 Sekunden sein. Für eine .NET-Lambda 500 ms–1,5 s.

V8 Isolates funktionieren anders:

  1. Anfrage kommt an
  2. Plattform erstellt ein neues V8 Isolate (oder verwendet ein warmes wieder)
  3. Dein Code wird geladen (er ist bereits zur Deploy-Zeit zu Bytecode kompiliert)
  4. Anfrage wird verarbeitet

Die Schritte 2–3 dauern unter 5 ms. Oft unter 1 ms. Das Isolate ist kein Container — es gibt kein Betriebssystem zum Booten, keine Runtime zum Initialisieren. V8 erstellt ein frisches Isolate in Mikrosekunden. Der Ausdruck „null Cold Starts" ist Marketing-Sprech, aber die Realität (unter 5 ms Startzeit) kommt nah genug an null heran, dass es für die meisten Anwendungsfälle keine Rolle spielt.

Aber hier beißen dich Cold Starts an der Edge doch noch:

Große Bundles. Wenn deine Edge Function 2 MB an Abhängigkeiten mitbringt, muss dieser Code trotzdem geladen und geparst werden. Das habe ich auf die harte Tour gelernt, als ich eine Validierungsbibliothek und eine Datumsformatierungsbibliothek in eine Edge-Middleware gebündelt habe. Der Cold Start stieg von 2 ms auf 40 ms. Immer noch schnell, aber nicht „null".

Seltene Standorte. Edge-Anbieter haben Hunderte von PoPs, aber nicht alle PoPs halten deinen Code warm. Wenn du eine Anfrage pro Stunde aus Nairobi bekommst, wird das Isolate zwischen den Anfragen recycelt. Die nächste Anfrage zahlt erneut die Startkosten.

Mehrere Isolates pro Anfrage. Wenn deine Edge Function eine andere Edge Function aufruft (oder wenn sowohl Middleware als auch eine API-Route Edge sind), spinnst du möglicherweise mehrere Isolates für eine einzelne Nutzeranfrage hoch.

Der praktische Rat: Halte deine Edge-Function-Bundles klein. Importiere nur, was du brauchst. Nutze Tree-Shaking aggressiv. Je kleiner das Bundle, desto schneller der Cold Start, desto besser hält das Versprechen „null Cold Starts".

typescript
// ❌ Das solltest du an der Edge nicht tun
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
 
// ✅ Mach es stattdessen so — nutze eingebaute APIs
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];

Perfekte Anwendungsfälle für Edge Functions#

Nach ausgiebigem Experimentieren habe ich ein klares Muster gefunden: Edge Functions glänzen, wenn du eine schnelle Entscheidung über eine Anfrage treffen musst, bevor sie deinen Origin-Server erreicht. Sie sind Torwächter, Router und Transformatoren — keine Anwendungsserver.

1. Geolokationsbasierte Weiterleitungen#

Das ist der Killer-Anwendungsfall. Die Anfrage trifft auf den nächsten Edge-Knoten, der bereits weiß, wo der Nutzer ist. Kein API-Call nötig, keine IP-Lookup-Datenbank — die Plattform liefert die Geodaten:

typescript
// middleware.ts — läuft an der Edge bei jeder Anfrage
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";
 
  // Weiterleitung zum länderspezifischen Shop
  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));
    }
  }
 
  // Geo-Header für nachgelagerte Nutzung hinzufügen
  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;
}

Das läuft in unter 5 ms, direkt neben dem Nutzer. Die Alternative — die Anfrage den ganzen Weg zu deinem Origin-Server zu schicken, nur um einen IP-Lookup durchzuführen und zurückzuleiten — würde 100–300 ms für Nutzer kosten, die weit von deinem Origin entfernt sind.

2. A/B-Testing ohne Client-Flicker#

Clientseitiges A/B-Testing verursacht das gefürchtete „Aufblitzen des Originalinhalts" — der Nutzer sieht Version A für den Bruchteil einer Sekunde, bevor JavaScript Version B einblendet. An der Edge kannst du die Variante zuweisen, bevor die Seite überhaupt mit dem Rendern beginnt:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Prüfen, ob der Nutzer bereits eine Variantenzuweisung hat
  const existingVariant = request.cookies.get("ab-variant")?.value;
 
  if (existingVariant) {
    // Umschreiben auf die korrekte Variantenseite
    const url = request.nextUrl.clone();
    url.pathname = `/variants/${existingVariant}${url.pathname}`;
    return NextResponse.rewrite(url);
  }
 
  // Neue Variante zuweisen (50/50 Aufteilung)
  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 Tage
    httpOnly: true,
    sameSite: "lax",
  });
 
  return response;
}

Der Nutzer sieht nie ein Flackern, weil das Rewrite auf Netzwerkebene passiert. Der Browser weiß nicht einmal, dass es ein A/B-Test war — er empfängt einfach direkt die Variantenseite.

3. Auth-Token-Verifizierung#

Wenn deine Authentifizierung JWTs verwendet (und du keine Datenbank-Session-Lookups machst), ist die Edge perfekt. JWT-Verifizierung ist reine Kryptografie — keine Datenbank nötig:

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",
    });
 
    // Nutzerinformationen als Header weiterleiten
    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 abgelaufen oder ungültig
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("session-token");
    return response;
  }
}

Dieses Muster ist wirkungsvoll: Die Edge-Middleware verifiziert den Token und gibt Nutzerinformationen als vertrauenswürdige Header an deinen Origin weiter. Deine API-Routen müssen den Token nicht erneut verifizieren — sie lesen einfach request.headers.get("x-user-id").

4. Bot-Erkennung und Rate Limiting#

Edge Functions können unerwünschten Traffic blockieren, bevor er jemals deinen Origin erreicht:

typescript
import { NextRequest, NextResponse } from "next/server";
 
// Einfacher In-Memory Rate Limiter (pro Edge-Standort)
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") ?? "";
 
  // Bekannte böswillige Bots blockieren
  const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
  if (badBots.some((bot) => ua.includes(bot))) {
    return new NextResponse("Forbidden", { status: 403 });
  }
 
  // Einfaches Rate Limiting
  const now = Date.now();
  const windowMs = 60_000; // 1 Minute
  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 });
  }
 
  // Periodische Bereinigung zur Vermeidung von Memory Leaks
  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();
}

Ein Vorbehalt: Die Rate-Limit-Map oben ist pro Isolate, pro Standort. Wenn du 300 Edge-Standorte hast, hat jeder seine eigene Map. Für striktes Rate Limiting brauchst du einen verteilten Speicher wie Upstash Redis oder Cloudflare Durable Objects. Aber für grobe Missbrauchsprävention funktionieren standortbezogene Limits erstaunlich gut.

5. Request Rewriting und Personalisierungsheader#

Edge Functions eignen sich hervorragend zum Transformieren von Anfragen, bevor sie deinen Origin erreichen:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl;
 
  // Gerätebasierte Content Negotiation
  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 aus Cookie
  const flags = request.cookies.get("feature-flags")?.value;
  if (flags) {
    response.headers.set("x-feature-flags", flags);
  }
 
  // Spracherkennung für 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;
}

Wo die Edge versagt#

Das ist der Abschnitt, den die Marketingseiten überspringen. Ich bin gegen jede dieser Wände gelaufen.

1. Datenbankverbindungen#

Das ist das große Problem. Traditionelle Datenbanken (PostgreSQL, MySQL) verwenden persistente TCP-Verbindungen. Ein Node.js-Server öffnet beim Start einen Connection Pool und verwendet diese Verbindungen über Requests hinweg wieder. Effizient, bewährt, gut verstanden.

Edge Functions können das nicht. Jedes Isolate ist kurzlebig. Es gibt keine „Startphase", in der du Verbindungen öffnest. Selbst wenn du eine Verbindung öffnen könntest, wird das Isolate möglicherweise nach einer einzigen Anfrage recycelt, wodurch die Verbindungsaufbauzeit verschwendet wird.

typescript
// ❌ Dieses Muster funktioniert an der Edge grundsätzlich nicht
import { Pool } from "pg";
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // Connection Pool mit 10 Verbindungen
});
 
// Jede Edge-Ausführung würde:
// 1. Einen neuen Pool erstellen (kann nicht zuverlässig über Aufrufe hinweg wiederverwendet werden)
// 2. Eine TCP-Verbindung zur Datenbank öffnen (die in us-east-1 steht, nicht an der Edge)
// 3. TLS-Handshake mit der Datenbank durchführen
// 4. Die Abfrage ausführen
// 5. Die Verbindung verwerfen, wenn das Isolate recycelt wird
 
// Selbst mit Connection-Pooling-Diensten wie PgBouncer
// zahlst du die Netzwerklatenz von Edge → Origin-Datenbank

Das Problem des Datenbank-Round-Trips ist fundamental. Deine Datenbank ist in einer Region. Deine Edge Function ist in 300 Regionen. Jede Datenbankabfrage von der Edge muss vom Edge-Standort zur Datenbankregion und zurück reisen. Für einen Nutzer in Tokio, der einen Tokio-Edge-Knoten erreicht, aber deine Datenbank in Virginia steht:

Edge Function in Tokio
  → Abfrage an PostgreSQL in Virginia: ~140ms Round Trip
  → Zweite Abfrage: ~140ms mehr
  → Gesamt: 280ms nur für zwei Abfragen

Node.js-Funktion in Virginia (gleiche Region wie DB)
  → Abfrage an PostgreSQL: ~1ms Round Trip
  → Zweite Abfrage: ~1ms mehr
  → Gesamt: 2ms für zwei Abfragen

Die Edge Function ist in diesem Szenario 140x langsamer für Datenbankoperationen. Es spielt keine Rolle, dass die Edge Function schneller gestartet ist — die Datenbank-Round-Trips dominieren alles.

Deshalb existieren HTTP-basierte Datenbank-Proxies (Neons Serverless-Treiber, PlanetScales Fetch-basierter Treiber, Supabases REST-API). Sie funktionieren, aber sie senden immer noch HTTP-Anfragen an eine Datenbank in einer einzelnen Region. Sie lösen das „kann kein TCP verwenden"-Problem, aber nicht das „Datenbank ist weit weg"-Problem.

typescript
// ✅ Das funktioniert an der Edge (HTTP-basierter Datenbankzugriff)
// Aber es ist trotzdem langsam, wenn die Datenbank weit vom Edge-Knoten entfernt ist
import { neon } from "@neondatabase/serverless";
 
export const runtime = "edge";
 
export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  // Das sendet eine HTTP-Anfrage an deine Neon-Datenbank
  // Funktioniert, aber die Latenz hängt von der Entfernung zur Datenbankregion ab
  const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
  return Response.json(posts);
}

2. Langlebige Aufgaben#

Edge Functions haben CPU-Zeitlimits, typischerweise 10–50 ms tatsächliche Rechenzeit. Die Wanduhrzeit ist großzügiger (meistens 30 Sekunden), aber CPU-intensive Operationen erreichen das Limit schnell:

typescript
// ❌ Diese werden die CPU-Zeitlimits an der Edge überschreiten
export const runtime = "edge";
 
export async function POST(request: Request) {
  const data = await request.json();
 
  // Bildverarbeitung — CPU-intensiv
  // (Außerdem kann sharp nicht verwendet werden, da es ein natives Modul ist)
  const processed = heavyImageProcessing(data.image);
 
  // PDF-Generierung — CPU-intensiv + braucht Node.js-APIs
  const pdf = generatePDF(data.content);
 
  // Große Datentransformation
  const result = data.items // 100.000 Elemente
    .map(transform)
    .filter(validate)
    .sort(compare)
    .reduce(aggregate, {});
 
  return Response.json(result);
}

Wenn deine Funktion mehr als ein paar Millisekunden CPU-Zeit braucht, gehört sie auf einen regionalen Node.js-Server. Punkt.

3. Nur-Node.js-Abhängigkeiten#

Das erwischt viele auf dem falschen Fuß. Eine überraschende Anzahl von npm-Paketen hängt von Node.js-Built-in-Modulen ab:

typescript
// ❌ Diese Pakete funktionieren nicht an der Edge
import bcrypt from "bcrypt";            // Natives C++-Binding
import sharp from "sharp";              // Natives C++-Binding
import puppeteer from "puppeteer";      // Braucht Dateisystem + child_process
import nodemailer from "nodemailer";    // Braucht net-Modul
import { readFile } from "fs/promises"; // Node.js-Dateisystem-API
import mongoose from "mongoose";         // TCP-Verbindungen + Node.js-APIs
 
// ✅ Edge-kompatible Alternativen
import { hashSync } from "bcryptjs";    // Reine JS-Implementierung (langsamer)
// Für Bilder: einen separaten Dienst oder API verwenden
// Für E-Mail: eine HTTP-basierte E-Mail-API verwenden (Resend, SendGrid REST)
// Für Datenbank: HTTP-basierte Clients verwenden

Bevor du irgendetwas an die Edge verschiebst, prüfe jede Abhängigkeit. Ein einziges require("fs"), das drei Ebenen tief in deinem Abhängigkeitsbaum vergraben ist, bringt deine Edge Function zur Laufzeit zum Absturz — nicht zur Build-Zeit. Du deployest, alles sieht gut aus, dann trifft die erste Anfrage diesen Codepfad und du bekommst einen kryptischen Fehler.

4. Große Bundlegrößen#

Edge-Plattformen haben strenge Bundlegrößen-Limits:

  • Cloudflare Workers: 1 MB (kostenlos), 5 MB (bezahlt)
  • Vercel Edge Functions: 4 MB (komprimiert)
  • Deno Deploy: 20 MB

Das klingt nach reichlich, bis du eine UI-Komponentenbibliothek, eine Validierungsbibliothek und eine Datumsbibliothek importierst. Ich hatte mal eine Edge-Middleware, die auf 3,5 MB angeschwollen ist, weil ich aus einer Barrel-Datei importiert habe, die das gesamte @/components-Verzeichnis mitgezogen hat.

typescript
// ❌ Barrel-File-Imports können viel zu viel mitziehen
import { validateEmail } from "@/lib/utils";
// Wenn utils.ts aus 20 anderen Modulen re-exportiert, werden alle gebündelt
 
// ✅ Direkt von der Quelle importieren
import { validateEmail } from "@/lib/validators/email";

5. Streaming und WebSockets#

Edge Functions können Streaming-Responses (Web Streams API) liefern, aber langlebige WebSocket-Verbindungen sind eine andere Geschichte. Während einige Plattformen WebSockets an der Edge unterstützen (Cloudflare Workers, Deno Deploy), macht die kurzlebige Natur von Edge Functions sie zu einer schlechten Wahl für zustandsbehaftete, langlebige Verbindungen.

Next.js Edge Runtime#

Next.js macht es unkompliziert, die Edge Runtime routenweise zu aktivieren. Du musst nicht alles umstellen — du wählst genau aus, welche Routen an der Edge laufen.

Middleware (immer Edge)#

Next.js Middleware läuft immer an der Edge. Das ist Absicht — Middleware fängt jede passende Anfrage ab, also muss sie schnell und global verteilt sein:

typescript
// middleware.ts — läuft immer an der Edge, kein Opt-in nötig
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Das läuft vor jeder passenden Anfrage
  // Halte es schnell — keine Datenbankaufrufe, keine schwere Berechnung
  return NextResponse.next();
}
 
export const config = {
  // Nur auf bestimmten Pfaden ausführen
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
  ],
};

API-Routen an der Edge#

Jeder Route Handler kann die Edge Runtime aktivieren:

typescript
// app/api/hello/route.ts
export const runtime = "edge"; // Diese eine Zeile ändert die Runtime
 
export async function GET(request: Request) {
  return Response.json({
    message: "Hello from the edge",
    region: process.env.VERCEL_REGION ?? "unknown",
    timestamp: Date.now(),
  });
}

Seitenrouten an der Edge#

Sogar ganze Seiten können an der Edge gerendert werden, obwohl ich mir das gut überlegen würde:

typescript
// app/dashboard/page.tsx
export const runtime = "edge";
 
export default async function DashboardPage() {
  // Beachte: Hier keine Node.js-APIs
  // Jedes Datenladen muss fetch() oder Edge-kompatible Clients verwenden
  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>
      {/* Daten rendern */}
    </main>
  );
}

Was in der Edge Runtime verfügbar ist#

Hier ist eine praktische Übersicht, was du verwenden kannst und was nicht:

typescript
// ✅ An der Edge verfügbar
fetch()                          // HTTP-Anfragen
Request / Response               // Web-Standard Request/Response
Headers                          // HTTP-Header
URL / URLSearchParams            // URL-Parsing
TextEncoder / TextDecoder        // String-Kodierung
crypto.subtle                    // Kryptografie-Operationen (Signieren, Hashen)
crypto.randomUUID()              // UUID-Generierung
crypto.getRandomValues()         // Kryptografische Zufallszahlen
structuredClone()                // Deep Cloning
atob() / btoa()                  // Base64-Kodierung/-Dekodierung
setTimeout() / setInterval()     // Timer (aber CPU-Limits beachten)
console.log()                    // Logging
ReadableStream / WritableStream  // Streaming
AbortController / AbortSignal    // Anfrage-Abbruch
URLPattern                       // URL-Pattern-Matching
 
// ❌ An der Edge NICHT verfügbar
require()                        // CommonJS (verwende import)
fs / path / os                   // Node.js-Built-in-Module
process.exit()                   // Prozesskontrolle
Buffer                           // Verwende stattdessen Uint8Array
__dirname / __filename           // Verwende import.meta.url
setImmediate()                   // Kein Webstandard

Auth an der Edge: Das vollständige Muster#

Ich möchte beim Thema Authentifizierung tiefer einsteigen, weil es einer der wirkungsvollsten Edge-Anwendungsfälle ist, aber auch leicht falsch gemacht werden kann.

Das Muster, das funktioniert: Verifiziere den Token an der Edge, gib vertrauenswürdige Claims weiter, berühre niemals die Datenbank in der Middleware.

typescript
// lib/edge-auth.ts — Edge-kompatible Auth-Utilities
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 Sekunden Toleranz für Uhrenabweichung
    });
 
    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 — Die Auth-Middleware
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;
 
  // Auth für öffentliche Pfade überspringen
  if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
    return NextResponse.next();
  }
 
  // Token extrahieren
  const token = request.cookies.get("auth-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  // Token verifizieren (reine Kryptografie — kein Datenbankaufruf)
  const payload = await verifyToken(token);
  if (!payload) {
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("auth-token");
    return response;
  }
 
  // Rollenbasierte Zugriffskontrolle
  if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }
 
  // Verifizierte Nutzerinformationen als vertrauenswürdige Header an den Origin weiterleiten
  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);
 
  // Signalisieren, wenn der Token erneuert werden muss
  if (isTokenExpiringSoon(payload)) {
    response.headers.set("x-token-refresh", "true");
  }
 
  return response;
}
typescript
// app/api/profile/route.ts — Origin-Server liest vertrauenswürdige Header
export async function GET(request: Request) {
  // Diese Header wurden von der Edge-Middleware nach JWT-Verifizierung gesetzt
  // Sie sind vertrauenswürdig, weil sie von unserer eigenen Infrastruktur stammen
  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 });
  }
 
  // Jetzt können wir auf die Datenbank zugreifen — wir sind auf dem Origin-Server,
  // direkt neben der Datenbank, mit einem Connection Pool
  const user = await db.user.findUnique({ where: { id: userId } });
 
  return Response.json(user);
}

Die entscheidende Erkenntnis: Die Edge erledigt den schnellen Teil (Krypto-Verifizierung), und der Origin erledigt den langsamen Teil (Datenbankabfragen). Jeder läuft dort, wo er am effizientesten ist.

Ein wichtiger Vorbehalt: Das funktioniert nur mit JWTs. Wenn dein Auth-System bei jeder Anfrage einen Datenbank-Lookup erfordert (wie sitzungsbasierte Authentifizierung mit einer Session-ID-Cookie), kann die Edge nicht helfen — du müsstest trotzdem die Datenbank abfragen, was einen Round Trip zur Origin-Region bedeutet.

Edge Caching#

Caching an der Edge ist der Punkt, an dem es richtig interessant wird. Edge-Knoten können Responses cachen, was bedeutet, dass nachfolgende Anfragen an dieselbe URL direkt von der Edge bedient werden, ohne deinen Origin überhaupt zu berühren.

Cache-Control richtig gemacht#

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: {
      // Auf CDN 60 Sekunden cachen
      // Stale servieren während der Revalidierung für bis zu 5 Minuten
      // Client kann 10 Sekunden cachen
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
 
      // Variiere nach diesen Headern, damit verschiedene Varianten verschiedene Cache-Einträge bekommen
      Vary: "Accept-Language, Accept-Encoding",
 
      // CDN-spezifisches Cache-Tag für gezielte Invalidierung
      "Cache-Tag": `products,category-${category}`,
    },
  });
}

Das stale-while-revalidate-Muster ist an der Edge besonders wirkungsvoll. So funktioniert es:

  1. Erste Anfrage: Edge holt vom Origin, cached die Antwort, gibt sie zurück
  2. Anfragen innerhalb von 60 Sekunden: Edge liefert aus dem Cache (0 ms Origin-Latenz)
  3. Anfrage nach 61–360 Sekunden: Edge liefert sofort die veraltete gecachte Version, holt aber im Hintergrund eine frische Version vom Origin
  4. Nach 360 Sekunden: Cache ist vollständig abgelaufen, nächste Anfrage geht zum Origin

Deine Nutzer bekommen fast immer eine gecachte Antwort. Der Frische-Kompromiss ist explizit und einstellbar.

Edge Config für dynamische Konfiguration#

Vercels Edge Config (und ähnliche Dienste anderer Plattformen) ermöglicht es, Key-Value-Konfiguration zu speichern, die an jeden Edge-Standort repliziert wird. Das ist unglaublich nützlich für Feature Flags, Weiterleitungsregeln und A/B-Test-Konfiguration, die du ohne Redeployment aktualisieren möchtest:

typescript
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
 
export async function middleware(request: NextRequest) {
  // Edge-Config-Lesevorgänge sind extrem schnell (~1ms), weil
  // die Daten an jeden Edge-Standort repliziert werden
  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));
  }
 
  // Dynamische Weiterleitungen (Weiterleitungen ohne Redeployment aktualisieren)
  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();
}

Das ist ein echter Gamechanger. Vor Edge Config bedeutete das Ändern eines Feature Flags eine Codeänderung und ein Redeployment. Jetzt aktualisierst du einen JSON-Wert in einem Dashboard und er wird in Sekunden weltweit propagiert.

Die ehrliche Performance-Rechnung#

Machen wir die ehrliche Rechnung statt der Marketing-Rechnung. Ich vergleiche drei Architekturen für einen typischen API-Endpunkt, der eine Datenbank abfragen muss:

Szenario: User-Profile-API (2 Datenbankabfragen)#

Architektur A: Traditionelles regionales Node.js

Nutzer in Tokio → Origin in Virginia: 140ms
  + DB-Abfrage 1 (gleiche Region): 2ms
  + DB-Abfrage 2 (gleiche Region): 2ms
  + Verarbeitung: 5ms
  = Gesamt: ~149ms

Architektur B: Edge Function mit HTTP-Datenbank

Nutzer in Tokio → Edge in Tokio: 5ms
  + DB-Abfrage 1 (HTTP nach Virginia): 145ms
  + DB-Abfrage 2 (HTTP nach Virginia): 145ms
  + Verarbeitung: 3ms
  = Gesamt: ~298ms ← LANGSAMER als regional

Architektur C: Edge Function mit regionaler Datenbank (Read Replica)

Nutzer in Tokio → Edge in Tokio: 5ms
  + DB-Abfrage 1 (HTTP zu Tokio-Replika): 8ms
  + DB-Abfrage 2 (HTTP zu Tokio-Replika): 8ms
  + Verarbeitung: 3ms
  = Gesamt: ~24ms ← Am schnellsten, aber erfordert Multi-Region-DB

Architektur D: Edge für Auth + Regional für Daten

Nutzer in Tokio → Edge-Middleware in Tokio: 5ms (JWT-Verifizierung)
  → Origin in Virginia: 140ms
  + DB-Abfrage 1 (gleiche Region): 2ms
  + DB-Abfrage 2 (gleiche Region): 2ms
  + Verarbeitung: 5ms
  = Gesamt: ~154ms
  (Aber Auth ist bereits verifiziert — Origin muss nicht erneut verifizieren)
  (Und unautorisierte Anfragen werden an der Edge blockiert — erreichen den Origin nie)

Die Erkenntnisse:

  1. Edge + Origin-Datenbank = oft langsamer als einfach einen regionalen Server zu nutzen
  2. Edge + Multi-Region-Datenbank = am schnellsten, aber am teuersten und komplexesten
  3. Edge als Torwächter + regional für Daten = bester pragmatischer Kompromiss
  4. Reine Edge (ohne Datenbank) = unschlagbar für Dinge wie Weiterleitungen und Auth-Checks

Architektur D ist das, was ich für die meisten Projekte verwende. Die Edge erledigt, was sie gut kann (schnelle Entscheidungen, Auth, Routing), und der regionale Node.js-Server erledigt, was er gut kann (Datenbankabfragen, schwere Berechnungen).

Wenn die Edge wirklich gewinnt: Operationen ohne Datenbank#

Die Rechnung dreht sich komplett um, wenn keine Datenbank involviert ist:

Weiterleitung (Edge):
  Nutzer in Tokio → Edge in Tokio → Weiterleitungsantwort: ~5ms

Weiterleitung (regional):
  Nutzer in Tokio → Origin in Virginia → Weiterleitungsantwort: ~280ms
Statische API-Antwort (Edge + Cache):
  Nutzer in Tokio → Edge in Tokio → gecachte Antwort: ~5ms

Statische API-Antwort (regional):
  Nutzer in Tokio → Origin in Virginia → Antwort: ~280ms
Bot-Blockierung (Edge):
  Böser Bot irgendwo → Edge (nächstgelegene) → 403-Antwort: ~5ms
  (Bot erreicht nie deinen Origin-Server)

Bot-Blockierung (regional):
  Böser Bot irgendwo → Origin in Virginia → 403-Antwort: ~280ms
  (Bot hat trotzdem Origin-Ressourcen verbraucht)

Für Operationen ohne Datenbank ist die Edge 20–50x schneller. Das ist kein Marketing — das ist Physik.

Mein Entscheidungsrahmen#

Nach einem Jahr Arbeit mit Edge Functions in der Produktion, hier ist der Entscheidungsbaum, den ich für jeden neuen Endpunkt oder jedes Stück Logik verwende:

Schritt 1: Braucht es Node.js-APIs?#

Wenn es fs, net, child_process oder ein natives Modul importiert — Node.js regional. Keine Diskussion.

Schritt 2: Braucht es Datenbankabfragen?#

Falls ja, und du hast keine Read Replicas in der Nähe deiner Nutzer — Node.js regional (in der gleichen Region wie deine Datenbank). Die Datenbank-Round-Trips werden alles dominieren.

Falls ja, und du hast global verteilte Read Replicas — Edge kann funktionieren, mit HTTP-basierten Datenbank-Clients.

Schritt 3: Ist es eine Entscheidung über eine Anfrage (Routing, Auth, Weiterleitung)?#

Falls ja — Edge. Das ist der Sweet Spot. Du triffst eine schnelle Entscheidung, die bestimmt, was mit der Anfrage passiert, bevor sie den Origin erreicht.

Schritt 4: Ist die Antwort cachebar?#

Falls ja — Edge mit passenden Cache-Control-Headern. Selbst wenn die erste Anfrage zu deinem Origin geht, werden nachfolgende Anfragen aus dem Edge-Cache bedient.

Schritt 5: Ist es CPU-intensiv?#

Wenn es um signifikante Berechnung geht (Bildverarbeitung, PDF-Generierung, große Datentransformationen) — Node.js regional.

Schritt 6: Wie latenzempfindlich ist es?#

Wenn es ein Hintergrundjob oder Webhook ist — Node.js regional. Niemand wartet darauf. Wenn es eine nutzergerichtete Anfrage ist, bei der jede Millisekunde zählt — Edge, wenn die anderen Kriterien erfüllt sind.

Der Spickzettel#

typescript
// ✅ PERFEKT für die Edge
// - Middleware (Auth, Weiterleitungen, Rewrites, Header)
// - Geolokationslogik
// - A/B-Test-Zuweisung
// - Bot-Erkennung / WAF-Regeln
// - Cache-freundliche API-Antworten
// - Feature-Flag-Prüfungen
// - CORS-Preflight-Antworten
// - Statische Datentransformationen (ohne DB)
// - Webhook-Signaturverifizierung
 
// ❌ AUF Node.js regional BELASSEN
// - Datenbank-CRUD-Operationen
// - Datei-Uploads / -Verarbeitung
// - Bildbearbeitung
// - PDF-Generierung
// - E-Mail-Versand (verwende HTTP-API, aber trotzdem regional)
// - WebSocket-Server
// - Hintergrundjobs / Queues
// - Alles, was native npm-Pakete nutzt
// - SSR-Seiten mit Datenbankabfragen
// - GraphQL-Resolver, die Datenbanken abfragen
 
// 🤔 ES KOMMT DRAUF AN
// - Authentifizierung (Edge für JWT, regional für Session-DB)
// - API-Routen (Edge ohne DB, regional mit DB)
// - Servergerenderte Seiten (Edge wenn Daten aus Cache/Fetch, regional bei DB)
// - Echtzeitfunktionen (Edge für initiale Auth, regional für persistente Verbindungen)

Was ich tatsächlich an der Edge betreibe#

Für diese Seite hier die Aufschlüsselung:

Edge (Middleware):

  • Spracherkennung und Weiterleitung
  • Bot-Filterung
  • Sicherheitsheader (CSP, HSTS usw.)
  • Zugriffsprotokollierung
  • Rate Limiting (grundlegend)

Node.js regional:

  • Blog-Content-Rendering (MDX-Verarbeitung braucht Node.js-APIs über Velite)
  • API-Routen, die Redis nutzen
  • OG-Bildgenerierung (braucht mehr CPU-Zeit)
  • RSS-Feed-Generierung

Statisch (überhaupt keine Runtime):

  • Tool-Seiten (beim Build vorgerendert)
  • Blog-Beitragsseiten (beim Build vorgerendert)
  • Alle Bilder und Assets (CDN-serviert)

Die beste Runtime ist oft gar keine Runtime. Wenn du etwas zur Build-Zeit vorrendern und als statisches Asset ausliefern kannst, wird das immer schneller sein als jede Edge Function. Die Edge ist für Dinge, die bei jeder Anfrage wirklich dynamisch sein müssen.

Das ehrliche Fazit#

Edge Functions sind kein Ersatz für traditionelle Server. Sie sind eine Ergänzung. Sie sind ein zusätzliches Werkzeug in deinem Architektur-Werkzeugkasten — eines, das für die richtigen Anwendungsfälle unglaublich mächtig und für die falschen aktiv schädlich ist.

Die Faustregel, zu der ich immer zurückkomme: Wenn deine Funktion eine Datenbank in einer einzelnen Region erreichen muss, hilft es nicht, die Funktion an die Edge zu stellen — es schadet. Du hast gerade einen Hop hinzugefügt. Die Funktion startet schneller, aber dann verbringt sie über 100 ms damit, zur Datenbank zurückzugreifen. Nettoergebnis: langsamer als alles in einer Region laufen zu lassen.

Aber für Entscheidungen, die nur mit den Informationen in der Anfrage selbst getroffen werden können — Geolokation, Cookies, Header, JWTs — ist die Edge unschlagbar. Diese 5-ms-Edge-Antworten sind keine synthetischen Benchmarks. Sie sind real, und deine Nutzer spüren den Unterschied.

Verschiebe nicht alles an die Edge. Halte nicht alles von der Edge fern. Platziere jedes Stück Logik dort, wo die Physik es begünstigt.

Ähnliche Beiträge