Back to all articles
API Gateway

Migrating from Self-Hosted to Managed API Gateway: A Complete Guide

February 28, 2026

Self-hosted API gateways like NGINX, Traefik, and Envoy are popular starting points. They're free, flexible, and give you full control. But as your API program grows, so does the operational burden of keeping them running — patching security vulnerabilities, managing configuration drift across environments, scaling infrastructure for traffic spikes, and cobbling together observability from a handful of open-source tools.

If you're starting to feel that pain, you're not alone. Many teams reach a point where the self-hosted gateway becomes the bottleneck, not the enabler. This guide walks you through every step of migrating from a self-hosted API gateway to a managed platform — from recognizing the signs, to mapping your existing configuration, to cutting over with zero downtime.

When It's Time to Migrate

Not every team needs to migrate immediately. But there are clear signals that your self-hosted gateway is holding you back:

  • The ops burden is growing faster than your API. You're spending more time upgrading NGINX modules, rotating TLS certificates, or debugging Traefik middleware chains than building API features.
  • Scaling is manual and reactive. Traffic spikes mean scrambling to add instances, tune connection pools, or restart pods. You've been burned by outages that a managed platform would have absorbed automatically.
  • Security patching is a full-time job. Every CVE in NGINX, OpenSSL, or your Lua/Go plugin dependencies triggers an emergency update cycle. You can't move fast when you're constantly firefighting.
  • Observability is stitched together. Your monitoring is a patchwork of Prometheus exporters, custom dashboards, and log aggregation pipelines that nobody fully understands.
  • Configuration drift is real. Your staging and production gateway configs have diverged in ways nobody can fully explain. Deployments are nerve-wracking because you're never 100% sure the config is correct.
  • You can't deploy globally. Your gateway runs in one or two regions. Users in other geographies experience high latency, and deploying additional instances means duplicating your entire infrastructure stack.

If more than two of these resonate, it's time to seriously evaluate a managed alternative.

What You Gain with a Managed Gateway

A managed API gateway eliminates the undifferentiated heavy lifting of infrastructure management while giving you capabilities that would take months to build in-house:

  • Automatic scaling. Traffic spikes are handled without intervention. No capacity planning, no pod autoscaler tuning, no database connection pool limits.
  • Built-in security. DDoS protection, automatic TLS, and regularly updated security policies — maintained by a dedicated security team, not yours.
  • Zero-downtime deployments. Push a config change and it rolls out globally without dropping a single request.
  • Global distribution. Your API runs at the edge, close to your users, with automatic failover across regions.
  • Developer portal. Auto-generated API documentation, interactive API explorer, and self-serve API key management — no separate tooling to build and maintain.
  • GitOps workflows. Routes and policies defined in code, version controlled, and deployed via CI/CD — giving you the same developer-centric workflow you value from managing config files in Git.

For a deeper look at why hosted gateways outperform self-managed solutions across cost, security, and customization, see Why a Hosted API Gateway Is Better Than Building Your Own.

Migration Planning Checklist

Before you touch any configuration, you need a complete inventory of what your current gateway does. Missed items become production incidents after migration.

Routes and Upstream Services

  • List every route (path + method) and its upstream backend
  • Document path rewriting rules and regex-based routing
  • Identify wildcard or catch-all routes
  • Note any WebSocket, gRPC, or Server-Sent Events endpoints

Security Policies

  • Authentication methods (API keys, JWT, OAuth 2.0, mTLS, basic auth)
  • Authorization rules (RBAC, IP allowlists, header-based access control)
  • TLS/SSL certificate configuration and renewal process
  • CORS policies per route or globally

Traffic Management

  • Rate limiting rules (per IP, per user, per API key, global)
  • Request/response size limits
  • Timeout configurations
  • Circuit breaker or retry policies

Request/Response Transformations

  • Header injection or removal
  • Request body transformations
  • Response body transformations (XML to JSON, field filtering)
  • URL rewriting

Custom Logic

  • Custom Lua scripts (NGINX/Kong)
  • Go middleware (Traefik)
  • C++ or Wasm filters (Envoy)
  • Any business logic embedded in the gateway layer

Observability and Monitoring

  • Logging format and destinations
  • Metrics and alerting rules
  • Distributed tracing configuration
  • Health check endpoints

Mapping Self-Hosted Concepts to a Managed Gateway

The biggest conceptual shift in migration is understanding how your existing gateway primitives translate to a managed platform's model. Here's how the core concepts map when migrating to Zuplo:

NGINX → Zuplo

ConceptNGINXZuplo Equivalent
Route definitionlocation blocks in nginx.confRoutes in routes.oas.json (OpenAPI format)
Upstream/backendupstream + proxy_passurlForwardHandler with baseUrl
Rate limitinglimit_req_zone + limit_reqrate-limit-inbound policy
Authenticationauth_request + Lua scriptsapi-key-inbound, open-id-jwt-auth-inbound, or custom policy
Request transformationproxy_set_header, LuaInbound policies (TypeScript)
Response transformationsub_filter, LuaOutbound policies (TypeScript)
TLS terminationssl_certificate directivesAutomatic (managed)
Custom business logicLua scripts via ngx_luaTypeScript policy modules
Config managementFiles on disk, often in GitGit-native (routes.oas.json + policies.json)
DocumentationSeparate tooling (Swagger UI, etc.)Built-in developer portal (auto-generated from OpenAPI)

Traefik → Zuplo

ConceptTraefikZuplo Equivalent
Route definitionIngressRoute CRDs or file providerRoutes in routes.oas.json (OpenAPI format)
Upstream/backendService definitionsurlForwardHandler with baseUrl
Rate limitingRateLimit middlewarerate-limit-inbound policy
AuthenticationForwardAuth middleware or OIDCapi-key-inbound, open-id-jwt-auth-inbound, or custom policy
Request transformationHeaders middlewareInbound policies (TypeScript)
Response transformationPlugin middlewareOutbound policies (TypeScript)
TLS terminationTLSStore / Let's EncryptAutomatic (managed)
Custom business logicGo middleware pluginsTypeScript policy modules
Config managementYAML files or K8s CRDsGit-native (routes.oas.json + policies.json)
DocumentationSeparate toolingBuilt-in developer portal (auto-generated from OpenAPI)

Envoy → Zuplo

ConceptEnvoyZuplo Equivalent
Route definitionroute_config in YAMLRoutes in routes.oas.json (OpenAPI format)
Upstream/backendclustersurlForwardHandler with baseUrl
Rate limitingenvoy.filters.http.ratelimitrate-limit-inbound policy
Authenticationext_authz filterapi-key-inbound, open-id-jwt-auth-inbound, or custom policy
Request transformationLua or Wasm filtersInbound policies (TypeScript)
Response transformationLua or Wasm filtersOutbound policies (TypeScript)
TLS terminationtransport_socketAutomatic (managed)
Custom business logicC++/Wasm/Lua filtersTypeScript policy modules
Config managementYAML + xDS APIGit-native (routes.oas.json + policies.json)
DocumentationSeparate toolingBuilt-in developer portal (auto-generated from OpenAPI)

For a broader comparison of API gateway hosting models and how different gateways compare, check out those dedicated guides.

Step-by-Step Migration from NGINX

NGINX is one of the most common self-hosted API gateways. Here's how a typical NGINX API gateway configuration translates to Zuplo.

NGINX Configuration (Before)

nginx
upstream api_backend {
    server backend1.example.com:8080;
    server backend2.example.com:8080;
}

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate /etc/ssl/certs/api.crt;
    ssl_certificate_key /etc/ssl/private/api.key;

    location /v1/users {
        limit_req zone=api_limit burst=20 nodelay;

        auth_request /auth;
        proxy_pass http://api_backend;
        proxy_set_header X-Request-ID $request_id;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /v1/products {
        limit_req zone=api_limit burst=50 nodelay;
        proxy_pass http://api_backend;
    }

    location = /auth {
        internal;
        proxy_pass http://auth-service:3000/validate;
    }
}

Zuplo Configuration (After)

In Zuplo, this configuration is split into two files: routes defined in OpenAPI format, and policies defined declaratively.

config/routes.oas.json — your routes:

JSONjson
{
  "openapi": "3.1.0",
  "info": {
    "title": "Example API",
    "version": "1.0.0"
  },
  "paths": {
    "/v1/users": {
      "x-zuplo-path": { "pathMode": "open-api" },
      "get": {
        "summary": "List Users",
        "operationId": "list-users",
        "x-zuplo-route": {
          "corsPolicy": "anything-goes",
          "handler": {
            "export": "urlForwardHandler",
            "module": "$import(@zuplo/runtime)",
            "options": {
              "baseUrl": "https://backend1.example.com:8080"
            }
          },
          "policies": {
            "inbound": ["api-key-auth", "rate-limit-standard"]
          }
        }
      }
    },
    "/v1/products": {
      "x-zuplo-path": { "pathMode": "open-api" },
      "get": {
        "summary": "List Products",
        "operationId": "list-products",
        "x-zuplo-route": {
          "corsPolicy": "anything-goes",
          "handler": {
            "export": "urlForwardHandler",
            "module": "$import(@zuplo/runtime)",
            "options": {
              "baseUrl": "https://backend1.example.com:8080"
            }
          },
          "policies": {
            "inbound": ["rate-limit-standard"]
          }
        }
      }
    }
  }
}

config/policies.json — your policies:

JSONjson
{
  "policies": [
    {
      "name": "api-key-auth",
      "policyType": "api-key-inbound",
      "handler": {
        "export": "ApiKeyInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {}
      }
    },
    {
      "name": "rate-limit-standard",
      "policyType": "rate-limit-inbound",
      "handler": {
        "export": "RateLimitInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {
          "rateLimitBy": "ip",
          "requestsAllowed": 10,
          "timeWindowMinutes": 1
        }
      }
    }
  ]
}

Notice what disappeared entirely: TLS certificate management, upstream health checks, load balancer configuration, and the auth subrequest plumbing. Zuplo handles all of that automatically.

Step-by-Step Migration from Traefik

Traefik's middleware model maps cleanly to Zuplo's policy pipeline. Here's how a typical Traefik configuration translates.

Traefik Configuration (Before)

YAMLyaml
# traefik.yml - Static configuration
entryPoints:
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: admin@example.com
      storage: acme.json
      httpChallenge:
        entryPoint: web

# Dynamic configuration
http:
  routers:
    api-users:
      rule: "Host(`api.example.com`) && PathPrefix(`/v1/users`)"
      service: api-backend
      middlewares:
        - rate-limit
        - forward-auth
      tls:
        certResolver: letsencrypt

  middlewares:
    rate-limit:
      rateLimit:
        average: 10
        period: 1s
        burst: 20
    forward-auth:
      forwardAuth:
        address: "http://auth-service:3000/validate"
        authResponseHeaders:
          - X-User-Id

  services:
    api-backend:
      loadBalancer:
        servers:
          - url: "http://backend1:8080"
          - url: "http://backend2:8080"

How the Traefik Config Maps to Zuplo

The same routes.oas.json and policies.json pattern from the NGINX example applies. Here's the equivalent Zuplo route for the Traefik api-users router above:

JSONjson
{
  "/v1/users": {
    "x-zuplo-path": { "pathMode": "open-api" },
    "get": {
      "summary": "List Users",
      "operationId": "list-users",
      "x-zuplo-route": {
        "corsPolicy": "anything-goes",
        "handler": {
          "export": "urlForwardHandler",
          "module": "$import(@zuplo/runtime)",
          "options": {
            "baseUrl": "https://backend1.example.com:8080"
          }
        },
        "policies": {
          "inbound": ["rate-limit-standard", "jwt-auth"]
        }
      }
    }
  }
}

The key differences to note:

  • TLS and certificate resolution — eliminated entirely. Zuplo manages TLS automatically.
  • Load balancing — handled by Zuplo's edge infrastructure. No server lists to maintain.
  • Middleware chaining — maps directly to Zuplo's inbound policy array. The execution order is the same: policies run in the order you list them.
  • ForwardAuth — replaced by built-in auth policies (api-key-inbound, open-id-jwt-auth-inbound) or a custom TypeScript policy for proprietary auth schemes.

If you have complex Traefik middleware chains, map each middleware to its Zuplo policy equivalent and list them in the inbound array in the same order.

Handling Custom Logic

This is where many migrations stall. If you've written custom plugins in Lua (NGINX/Kong), Go (Traefik), or C++/Wasm (Envoy), you need to rewrite them. The good news: TypeScript is a significant upgrade in readability and maintainability.

Example: Custom Lua Script in NGINX

lua
-- Custom rate limiting based on API key tier
local function get_rate_limit()
    local api_key = ngx.req.get_headers()["X-API-Key"]
    local redis = require "resty.redis"
    local red = redis:new()
    red:connect("127.0.0.1", 6379)

    local tier = red:get("tier:" .. api_key)
    if tier == "premium" then
        return 1000
    else
        return 100
    end
end

Equivalent TypeScript Policy in Zuplo

In Zuplo, you implement this as a custom rate limit identifier function — a TypeScript module that the built-in rate-limit-inbound policy calls to determine per-request limits:

TypeScripttypescript
// modules/rate-limiter.ts
import {
  CustomRateLimitDetails,
  ZuploContext,
  ZuploRequest,
} from "@zuplo/runtime";

export function rateLimitIdentifier(
  request: ZuploRequest,
  context: ZuploContext,
  policyName: string,
): CustomRateLimitDetails {
  const user = request.user;

  if (user?.data?.tier === "premium") {
    return {
      key: user.sub,
      requestsAllowed: 1000,
      timeWindowMinutes: 1,
    };
  }

  return {
    key: user?.sub ?? request.headers.get("x-api-key") ?? "anonymous",
    requestsAllowed: 100,
    timeWindowMinutes: 1,
  };
}

Wire it up in config/policies.json using the rateLimitBy: "function" option:

JSONjson
{
  "name": "tiered-rate-limit",
  "policyType": "rate-limit-inbound",
  "handler": {
    "export": "RateLimitInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "rateLimitBy": "function",
      "requestsAllowed": 100,
      "timeWindowMinutes": 1,
      "identifier": {
        "module": "$import(./modules/rate-limiter)",
        "export": "rateLimitIdentifier"
      }
    }
  }
}

The TypeScript version is shorter, type-safe, and doesn't require managing a Redis connection — Zuplo's rate limiting infrastructure handles the distributed counting. No more debugging Lua coroutines or managing C++ build toolchains.

Migration Tips for Custom Logic

  • Start with built-in policies. Zuplo has dozens of built-in policies covering authentication, rate limiting, request validation, caching, and more. Check if your custom logic already has a built-in equivalent before rewriting.
  • Use TypeScript modules. Custom policies are standard TypeScript files in your project's modules/ directory. They have full access to the Request, Response, and ZuploContext objects.
  • Leverage the policy pipeline. Instead of one monolithic plugin that does everything, break your logic into composable inbound and outbound policies.

Zero-Downtime Migration Strategies

The biggest risk in any gateway migration is dropping production traffic. Here are three strategies for cutting over safely.

Strategy 1: DNS-Based Blue-Green

The simplest approach. Run your new managed gateway in parallel with your existing self-hosted gateway, then switch DNS.

  1. Set up Zuplo with all your routes, policies, and custom logic.
  2. Test thoroughly using the preview environment (every PR gets its own isolated environment in Zuplo).
  3. Lower DNS TTL on your API domain to 60 seconds, 24 hours before cutover.
  4. Switch DNS to point at your Zuplo gateway.
  5. Monitor for errors and latency changes.
  6. Roll back by reverting DNS if anything goes wrong.

Best for: Teams with simple routing who want the lowest-risk approach.

Strategy 2: Canary Routing with Percentage-Based Rollout

Route a small percentage of traffic to the new gateway and gradually increase it.

With Zuplo, you can implement percentage-based canary routing using a custom inbound policy that hashes a stable client identifier and routes based on a configurable percentage:

TypeScripttypescript
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";

function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = (hash << 5) - hash + char;
    hash |= 0;
  }
  return Math.abs(hash);
}

export default async function canaryPolicy(
  request: ZuploRequest,
  context: ZuploContext,
) {
  const canaryPercentage = parseInt(environment.CANARY_PERCENTAGE ?? "0", 10);
  const clientId =
    request.headers.get("x-session-id") ??
    request.headers.get("x-forwarded-for") ??
    "default";

  const hashValue = simpleHash(clientId) % 100;

  if (hashValue < canaryPercentage) {
    context.custom.backendUrl = environment.API_URL_CANARY;
    context.log.info(`Routed to canary (hash: ${hashValue})`);
  } else {
    context.custom.backendUrl = environment.API_URL_PRODUCTION;
  }

  return request;
}

Start at 5%, monitor, increase to 25%, then 50%, then 100%. This approach gives you fine-grained control and sticky routing per client. For more on this pattern, see What Is Canary Routing?.

Best for: High-traffic APIs where you need to validate performance under real load before full cutover.

Strategy 3: Shadow Traffic Testing

Forward a copy of production traffic to the new gateway without affecting the response path.

  1. Configure your existing gateway to mirror requests to Zuplo.
  2. Compare responses, latency, and error rates between old and new.
  3. Fix any discrepancies.
  4. Once parity is confirmed, cut over using DNS or canary routing.

Best for: Mission-critical APIs where you need absolute confidence before any traffic switch.

Post-Migration Validation

After cutover, run through this validation checklist:

Functional Validation

  • Every route returns the expected response codes
  • Authentication works for all methods (API key, JWT, OAuth)
  • Rate limiting triggers at the correct thresholds
  • Request and response transformations produce identical output
  • Error responses match the expected format
  • CORS headers are correct for all origins

Performance Validation

  • P50, P95, and P99 latency is equal to or better than the old gateway
  • Throughput handles peak traffic without degradation
  • No increased error rates under load

Observability Validation

  • Logs are flowing to your log aggregation system
  • Metrics are visible in your monitoring dashboards
  • Alerts trigger correctly for error rate and latency thresholds

Security Validation

  • TLS is terminating correctly (verify certificate chain)
  • Unauthorized requests are rejected
  • Rate limiting blocks excessive traffic
  • No unintended routes are exposed

Common Migration Pitfalls and How to Avoid Them

1. Incomplete Route Inventory

The mistake: You migrate the routes you know about and miss the ones buried in ancient NGINX config includes or Kubernetes annotations.

The fix: Use automated tooling to extract routes. For NGINX, parse all included config files. For Traefik, export all IngressRoute CRDs. For Envoy, dump the route table via the admin API. Cross-reference with access logs to identify routes that still receive traffic.

2. Ignoring Implicit Behavior

The mistake: Self-hosted gateways have implicit behaviors — NGINX's default buffering, Traefik's automatic HTTPS redirects, Envoy's default timeouts. You don't realize you depend on them until they're gone.

The fix: Document not just what your gateway does, but what it does by default. Test edge cases: large request bodies, slow backends, WebSocket upgrades, and connection timeouts.

3. Big-Bang Cutover

The mistake: Migrating everything at once on a Friday afternoon.

The fix: Migrate route by route or service by service. Start with low-traffic, low-risk endpoints. Use canary routing to validate each batch before moving to the next.

4. Not Testing Under Real Load

The mistake: Your test suite passes, so you assume production will work.

The fix: Use shadow traffic or replay production logs against the new gateway. Synthetic tests don't catch the weird edge cases that real traffic exposes.

5. Losing Custom Headers or Transformations

The mistake: Your NGINX config adds X-Request-ID headers, your Lua script strips sensitive headers from responses, or your Traefik middleware rewrites paths. These subtle transformations are easy to miss.

The fix: Capture request/response pairs from your existing gateway using access logs or traffic capture. Replay them against the new gateway and diff the results.

Making the Move

Migrating from a self-hosted API gateway to a managed platform is a significant undertaking, but it pays dividends in reduced operational burden, better security, and faster development velocity. The key is to approach it methodically: inventory everything, migrate incrementally, and validate thoroughly.

Zuplo is designed specifically for teams making this transition. The GitOps workflow feels familiar if you're used to managing NGINX or Traefik configs in Git. TypeScript policies replace complex Lua or Go plugins with readable, type-safe code. And the OpenAPI-native routing means you can import your existing API specs to generate routes and a developer portal instantly.

If you're evaluating your options, check out our comparison of API gateway hosting options and the build vs. buy decision framework to see how managed gateways stack up against self-hosted solutions. When you're ready to start, sign up for free and deploy your first API to 300+ edge locations in minutes.

Tags:#API Gateway#API Best Practices

Related Articles

Continue learning from the Zuplo Learning Center.

Edge Computing

Edge-Native API Gateway Architecture: Benefits, Patterns, and Use Cases

Learn what edge-native API gateways are, how they differ from cloud-region gateways, and why they deliver lower latency, better security, and global scale.

API Gateway

Zuplo vs Postman: API Gateway vs API Development Platform — What's the Difference?

Zuplo vs Postman — understand the difference between an API gateway and an API development platform, when to use each, and how they work together.

On this page

When It's Time to MigrateWhat You Gain with a Managed GatewayMigration Planning ChecklistMapping Self-Hosted Concepts to a Managed GatewayStep-by-Step Migration from NGINXStep-by-Step Migration from TraefikHandling Custom LogicZero-Downtime Migration StrategiesPost-Migration ValidationCommon Migration Pitfalls and How to Avoid ThemMaking the Move

Scale your APIs with
confidence.

Start for free or book a demo with our team.
Book a demoStart for Free
SOC 2 TYPE 2High Performer Spring 2025Momentum Leader Spring 2025Best Estimated ROI Spring 2025Easiest To Use Spring 2025Fastest Implementation Spring 2025

Get Updates From Zuplo

Zuplo logo
© 2026 zuplo. All rights reserved.
Products & Features
API ManagementAI GatewayMCP ServersMCP GatewayDeveloper PortalRate LimitingOpenAPI NativeGitOpsProgrammableAPI Key ManagementMulti-cloudAPI GovernanceMonetizationSelf-Serve DevX
Developers
DocumentationBlogLearning CenterCommunityChangelogIntegrations
Product
PricingSupportSign InCustomer Stories
Company
About UsMedia KitCareersStatusTrust & Compliance
Privacy PolicySecurity PoliciesTerms of ServiceTrust & Compliance
Docs
Pricing
Sign Up
Login
ContactBook a demoFAQ
Zuplo logo
DocsPricingSign Up
Login