Skip to main content

Unkey vs Zuplo vs Kong Gateway: API Gateway Platforms Compared (2026)

·PkgPulse Team

TL;DR: Unkey is the open-source API key management platform — sub-millisecond key verification, built-in rate limiting, usage analytics, and temporary keys with no gateway to deploy. Zuplo is the programmable API gateway built on Cloudflare Workers — edge-deployed, TypeScript policies, OpenAPI-native, and developer portal included. Kong Gateway is the enterprise API gateway — plugin ecosystem, service mesh, multi-protocol support, and the most mature self-hosted option. In 2026: Unkey for API key management without a full gateway, Zuplo for edge-native programmable gateways, Kong for enterprise API infrastructure.

Key Takeaways

  • Unkey: Open-source (Apache 2.0), key management focused. API key creation/verification, rate limiting, usage tracking, temporary keys. Not a full gateway — integrates into your existing API. Best for adding API key auth and rate limiting without deploying infrastructure
  • Zuplo: Cloud-native, edge-deployed. TypeScript request/response policies, OpenAPI-first, built-in developer portal, Cloudflare Workers runtime. Best for API products needing a gateway with developer portal and edge performance
  • Kong Gateway: Open-source + enterprise. Plugin architecture, service mesh (Kuma), multi-protocol (REST, gRPC, GraphQL, WebSocket), Kubernetes-native. Best for enterprise API infrastructure with complex routing and plugin needs

Unkey — API Key Management

Unkey gives you API key management, rate limiting, and usage tracking without deploying a gateway — just verify keys inline.

Creating and Managing API Keys

import { Unkey } from "@unkey/api";

const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY! });

// Create an API key
const { result } = await unkey.keys.create({
  apiId: process.env.UNKEY_API_ID!,
  prefix: "sk_live", // Key prefix for identification
  name: "Acme Corp Production",
  ownerId: "customer_42", // Link to your customer
  meta: {
    plan: "enterprise",
    company: "Acme Corp",
  },
  // Rate limiting
  ratelimit: {
    type: "fast", // "fast" (local) or "consistent" (global)
    limit: 1000,
    refillRate: 100, // 100 tokens per interval
    refillInterval: 1000, // every 1 second
  },
  // Auto-expire after 90 days
  expires: Date.now() + 90 * 24 * 60 * 60 * 1000,
  // Usage limits
  remaining: 1000000, // Max 1M requests total
});

console.log(`Key: ${result.key}`); // sk_live_abc123...
console.log(`Key ID: ${result.keyId}`);

Verifying API Keys

import { verifyKey } from "@unkey/api";

// Verify in your API handler — sub-millisecond
async function authenticateRequest(req: Request): Promise<{
  valid: boolean;
  ownerId?: string;
  meta?: Record<string, unknown>;
  ratelimit?: { remaining: number };
}> {
  const apiKey = req.headers.get("Authorization")?.replace("Bearer ", "");
  if (!apiKey) return { valid: false };

  const { result, error } = await verifyKey({
    key: apiKey,
    apiId: process.env.UNKEY_API_ID!,
  });

  if (error || !result.valid) {
    return { valid: false };
  }

  return {
    valid: true,
    ownerId: result.ownerId,
    meta: result.meta,
    ratelimit: result.ratelimit
      ? { remaining: result.ratelimit.remaining }
      : undefined,
  };
}

// Express middleware
function unkeyAuth(req: Request, res: Response, next: NextFunction) {
  authenticateRequest(req).then(({ valid, ownerId, meta, ratelimit }) => {
    if (!valid) {
      return res.status(401).json({ error: "Invalid API key" });
    }

    if (ratelimit && ratelimit.remaining <= 0) {
      return res.status(429).json({ error: "Rate limit exceeded" });
    }

    req.customerId = ownerId;
    req.plan = meta?.plan;
    next();
  });
}

app.use("/api/*", unkeyAuth);

Rate Limiting

// Standalone rate limiting (without API keys)
import { Ratelimit } from "@unkey/ratelimit";

const limiter = new Ratelimit({
  rootKey: process.env.UNKEY_ROOT_KEY!,
  namespace: "api.requests",
  limit: 100,
  duration: "60s", // 100 requests per 60 seconds
});

app.use("/api/*", async (req, res, next) => {
  const identifier = req.ip || req.headers["x-forwarded-for"];
  const { success, remaining, reset } = await limiter.limit(identifier);

  res.setHeader("X-RateLimit-Remaining", remaining);
  res.setHeader("X-RateLimit-Reset", reset);

  if (!success) {
    return res.status(429).json({
      error: "Rate limit exceeded",
      retryAfter: Math.ceil((reset - Date.now()) / 1000),
    });
  }

  next();
});

// Tiered rate limiting based on plan
async function tierLimiter(req: Request, res: Response, next: NextFunction) {
  const plan = req.plan || "free";
  const limits = {
    free: { limit: 100, duration: "60s" },
    pro: { limit: 1000, duration: "60s" },
    enterprise: { limit: 10000, duration: "60s" },
  };

  const config = limits[plan];
  const ratelimit = new Ratelimit({
    rootKey: process.env.UNKEY_ROOT_KEY!,
    namespace: `api.${plan}`,
    ...config,
  });

  const { success, remaining } = await ratelimit.limit(req.customerId);
  if (!success) return res.status(429).json({ error: "Rate limit exceeded" });

  res.setHeader("X-RateLimit-Remaining", remaining);
  next();
}

Key Analytics and Management

// List keys for a customer
const { result: keys } = await unkey.keys.list({
  apiId: process.env.UNKEY_API_ID!,
  ownerId: "customer_42",
});

for (const key of keys.keys) {
  console.log(`${key.name}: ${key.start}... (${key.remaining} remaining)`);
}

// Update a key
await unkey.keys.update({
  keyId: keyId,
  ratelimit: {
    type: "fast",
    limit: 5000, // Upgrade rate limit
    refillRate: 500,
    refillInterval: 1000,
  },
  meta: { plan: "enterprise-plus" },
});

// Revoke a key
await unkey.keys.delete({ keyId: keyId });

// Get usage analytics
const { result: analytics } = await unkey.keys.getVerifications({
  keyId: keyId,
  start: Date.now() - 7 * 24 * 60 * 60 * 1000, // Last 7 days
  granularity: "day",
});

for (const day of analytics.verifications) {
  console.log(`${day.time}: ${day.success} ok, ${day.rateLimited} limited`);
}

Zuplo — Edge-Native Programmable Gateway

Zuplo is a programmable API gateway deployed to 300+ edge locations — TypeScript policies, OpenAPI-first design, and a built-in developer portal.

Gateway Configuration

// routes.oas.json — OpenAPI-based route configuration
{
  "openapi": "3.1.0",
  "info": { "title": "Acme API", "version": "2.0" },
  "paths": {
    "/v2/projects": {
      "get": {
        "operationId": "listProjects",
        "x-zuplo-route": {
          "handler": {
            "module": "$import(@zuplo/runtime)",
            "export": "urlForwardHandler",
            "options": {
              "baseUrl": "https://api-internal.acme.com"
            }
          },
          "policies": {
            "inbound": [
              "api-key-auth",
              "rate-limit-inbound",
              "request-validation"
            ],
            "outbound": [
              "remove-internal-headers"
            ]
          }
        }
      }
    }
  }
}

Custom TypeScript Policies

// modules/policies/custom-auth.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function customAuth(
  request: ZuploRequest,
  context: ZuploContext,
  options: { requiredScopes?: string[] },
  policyName: string
) {
  const apiKey = request.headers.get("authorization")?.replace("Bearer ", "");

  if (!apiKey) {
    return new Response(JSON.stringify({ error: "Missing API key" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Verify key against your auth system
  const keyInfo = await context.cache.get(`key:${apiKey}`);

  if (!keyInfo) {
    // Cache miss — verify against backend
    const verification = await fetch("https://auth.acme.com/verify", {
      method: "POST",
      body: JSON.stringify({ key: apiKey }),
    });

    if (!verification.ok) {
      return new Response(JSON.stringify({ error: "Invalid API key" }), {
        status: 401,
      });
    }

    const data = await verification.json();
    await context.cache.set(`key:${apiKey}`, JSON.stringify(data), 300); // 5 min cache
  }

  // Check scopes
  if (options.requiredScopes) {
    const scopes = keyInfo.scopes || [];
    const hasScope = options.requiredScopes.every((s) => scopes.includes(s));
    if (!hasScope) {
      return new Response(JSON.stringify({ error: "Insufficient scope" }), {
        status: 403,
      });
    }
  }

  // Attach user info to request for downstream handlers
  request.user = keyInfo;
  return request; // Continue to next policy/handler
}

Request/Response Transformation

// modules/policies/transform-response.ts
import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function transformResponse(
  response: Response,
  request: ZuploRequest,
  context: ZuploContext,
  options: {},
  policyName: string
) {
  const body = await response.json();

  // Transform the response
  const transformed = {
    data: body.results,
    pagination: {
      total: body.total_count,
      page: body.page,
      perPage: body.per_page,
      nextCursor: body.next_cursor,
    },
    meta: {
      requestId: context.requestId,
      timestamp: new Date().toISOString(),
    },
  };

  return new Response(JSON.stringify(transformed), {
    status: response.status,
    headers: {
      "Content-Type": "application/json",
      "X-Request-Id": context.requestId,
      "Cache-Control": "public, max-age=60",
    },
  });
}

Built-in Developer Portal

// zuplo.jsonc — developer portal configuration
{
  "developerPortal": {
    "enabled": true,
    "pageTitle": "Acme API Documentation",
    "faviconUrl": "/public/favicon.ico",
    "logoUrl": "/public/logo.svg",
    "theme": {
      "primary": "#0D9373",
      "background": "#0a0a0a"
    },
    "authentication": {
      "provider": "auth0",
      "issuer": "https://acme.auth0.com/",
      "clientId": "abc123"
    },
    "generateExamples": true,
    "enableKeyManagement": true
  }
}

Kong Gateway — Enterprise API Infrastructure

Kong Gateway is the enterprise-grade API gateway — plugin ecosystem, service mesh integration, and multi-protocol support.

Kubernetes Ingress Configuration

# Kong Ingress Controller — Kubernetes-native
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: rate-limiting
config:
  minute: 100
  policy: redis
  redis:
    host: redis.default.svc.cluster.local
    port: 6379
plugin: rate-limiting

---
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: key-auth
plugin: key-auth

---
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: cors
config:
  origins: ["https://app.acme.com"]
  methods: ["GET", "POST", "PUT", "DELETE"]
  headers: ["Authorization", "Content-Type"]
  max_age: 3600
plugin: cors

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway
  annotations:
    konghq.com/plugins: rate-limiting, key-auth, cors
    konghq.com/strip-path: "true"
spec:
  ingressClassName: kong
  rules:
    - host: api.acme.com
      http:
        paths:
          - path: /v2/projects
            pathType: Prefix
            backend:
              service:
                name: project-service
                port:
                  number: 8080
          - path: /v2/users
            pathType: Prefix
            backend:
              service:
                name: user-service
                port:
                  number: 8080

Admin API

// Kong Admin API — manage configuration
const KONG_ADMIN = "http://kong-admin:8001";

// Create a service
await fetch(`${KONG_ADMIN}/services`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "project-service",
    url: "http://project-svc.default.svc.cluster.local:8080",
    retries: 3,
    connect_timeout: 5000,
    read_timeout: 30000,
  }),
});

// Create a route
await fetch(`${KONG_ADMIN}/services/project-service/routes`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "projects-route",
    paths: ["/v2/projects"],
    methods: ["GET", "POST", "PUT", "DELETE"],
    strip_path: true,
    protocols: ["https"],
  }),
});

// Enable plugins on a service
await fetch(`${KONG_ADMIN}/services/project-service/plugins`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "rate-limiting",
    config: {
      minute: 1000,
      hour: 50000,
      policy: "redis",
      redis: { host: "redis", port: 6379 },
    },
  }),
});

// JWT authentication plugin
await fetch(`${KONG_ADMIN}/services/project-service/plugins`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "jwt",
    config: {
      claims_to_verify: ["exp"],
      key_claim_name: "iss",
      header_names: ["Authorization"],
    },
  }),
});

// Request transformation
await fetch(`${KONG_ADMIN}/services/project-service/plugins`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "request-transformer",
    config: {
      add: {
        headers: ["X-Request-ID:$(uuid)"],
        querystring: ["version:v2"],
      },
      remove: {
        headers: ["X-Internal-Token"],
      },
    },
  }),
});

Custom Plugin (Lua)

-- kong/plugins/custom-auth/handler.lua
local CustomAuth = {
  PRIORITY = 1000,
  VERSION = "1.0.0",
}

function CustomAuth:access(conf)
  local api_key = kong.request.get_header("X-API-Key")

  if not api_key then
    return kong.response.exit(401, { message = "Missing API key" })
  end

  -- Verify against external service
  local httpc = require("resty.http").new()
  local res, err = httpc:request_uri(conf.auth_url, {
    method = "POST",
    body = kong.table.new(0, 1),
    headers = {
      ["Content-Type"] = "application/json",
    },
  })

  if not res or res.status ~= 200 then
    return kong.response.exit(403, { message = "Invalid API key" })
  end

  -- Set consumer headers for upstream
  local body = require("cjson").decode(res.body)
  kong.service.request.set_header("X-Consumer-ID", body.customer_id)
  kong.service.request.set_header("X-Consumer-Plan", body.plan)
end

return CustomAuth

Feature Comparison

FeatureUnkeyZuploKong Gateway
TypeAPI key managementProgrammable gatewayFull API gateway
DeploymentCloud (no infra)Edge (300+ PoPs)Self-hosted or cloud
LicenseApache 2.0ProprietaryApache 2.0 + Enterprise
API Key Management✅ (core feature)✅ (built-in)Plugin (key-auth)
Rate Limiting✅ (global + local)✅ (edge-native)Plugin (Redis-backed)
Request Routing❌ (not a gateway)
Request Transformation✅ (TypeScript)✅ (plugins)
AuthenticationAPI keysAPI keys, JWT, OAuthAPI keys, JWT, OAuth, OIDC, LDAP
Developer Portal✅ (built-in)Kong Portal (Enterprise)
Custom LogicN/ATypeScript policiesLua plugins
OpenAPI Support✅ (OpenAPI-first)Spec-based routes
gRPC Support
WebSocket Support
Service Mesh✅ (Kuma)
Analytics✅ (key usage)✅ (request analytics)✅ (Vitals - Enterprise)
Kubernetes Native✅ (Ingress Controller)
Plugin EcosystemN/APolicies (TypeScript)100+ plugins
Latency Overhead<1ms (key verify)<5ms (edge)1-10ms
ComplexityVery lowLowMedium-High
Best ForAPI key auth/limitsEdge API gatewayEnterprise infrastructure

When to Use Each

Choose Unkey if:

  • You need API key management without deploying a gateway
  • Sub-millisecond key verification with built-in rate limiting is important
  • Temporary keys with auto-expiry and usage caps fit your use case
  • You already have an API and just need auth + rate limiting added
  • Per-key analytics and usage tracking drive billing or monitoring

Choose Zuplo if:

  • You're building an API product and need a gateway with developer portal
  • Edge deployment (300+ locations) for low-latency global access matters
  • TypeScript-based request/response policies give your team flexibility
  • OpenAPI-first design with automatic validation is important
  • You want a modern gateway that deploys like a Cloudflare Worker

Choose Kong Gateway if:

  • Enterprise API infrastructure with 100+ plugins is needed
  • Multi-protocol support (REST, gRPC, GraphQL, WebSocket) is important
  • Kubernetes Ingress Controller for service mesh integration matters
  • Self-hosted deployment for compliance and data residency is required
  • You need service mesh capabilities (Kuma) alongside API gateway

Methodology

Feature comparison based on Unkey, Zuplo, and Kong Gateway (OSS + Enterprise) documentation as of March 2026. Unkey evaluated on key management API, rate limiting, and verification latency. Zuplo evaluated on edge performance, policy system, and developer portal. Kong evaluated on plugin ecosystem, protocol support, and Kubernetes integration. Code examples use official SDKs and REST APIs.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.