---
title: "Migrating from Self-Hosted to Managed API Gateway: A Complete Guide"
description: "A practical guide to migrating from self-hosted API gateways like NGINX, Traefik, or Envoy to a managed gateway — covering planning, config mapping, and zero-downtime strategies."
canonicalUrl: "https://zuplo.com/learning-center/migrate-self-hosted-to-managed-api-gateway"
pageType: "learning-center"
authors: "nate"
tags: "API Gateway, API Best Practices"
image: "https://zuplo.com/og?text=Migrating%20from%20Self-Hosted%20to%20Managed%20API%20Gateway%3A%20A%20Complete%20Guide"
---
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](/learning-center/hosted-api-gateway-advantages).

## 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**

| Concept                 | NGINX                               | Zuplo Equivalent                                                |
| :---------------------- | :---------------------------------- | :-------------------------------------------------------------- |
| Route definition        | `location` blocks in `nginx.conf`   | Routes in `routes.oas.json` (OpenAPI format)                    |
| Upstream/backend        | `upstream` + `proxy_pass`           | `urlForwardHandler` with `baseUrl`                              |
| Rate limiting           | `limit_req_zone` + `limit_req`      | `rate-limit-inbound` policy                                     |
| Authentication          | `auth_request` + Lua scripts        | `api-key-inbound`, `open-id-jwt-auth-inbound`, or custom policy |
| Request transformation  | `proxy_set_header`, Lua             | Inbound policies (TypeScript)                                   |
| Response transformation | `sub_filter`, Lua                   | Outbound policies (TypeScript)                                  |
| TLS termination         | `ssl_certificate` directives        | Automatic (managed)                                             |
| Custom business logic   | Lua scripts via `ngx_lua`           | TypeScript policy modules                                       |
| Config management       | Files on disk, often in Git         | Git-native (`routes.oas.json` + `policies.json`)                |
| Documentation           | Separate tooling (Swagger UI, etc.) | Built-in developer portal (auto-generated from OpenAPI)         |

**Traefik → Zuplo**

| Concept                 | Traefik                              | Zuplo Equivalent                                                |
| :---------------------- | :----------------------------------- | :-------------------------------------------------------------- |
| Route definition        | `IngressRoute` CRDs or file provider | Routes in `routes.oas.json` (OpenAPI format)                    |
| Upstream/backend        | `Service` definitions                | `urlForwardHandler` with `baseUrl`                              |
| Rate limiting           | `RateLimit` middleware               | `rate-limit-inbound` policy                                     |
| Authentication          | `ForwardAuth` middleware or OIDC     | `api-key-inbound`, `open-id-jwt-auth-inbound`, or custom policy |
| Request transformation  | `Headers` middleware                 | Inbound policies (TypeScript)                                   |
| Response transformation | `Plugin` middleware                  | Outbound policies (TypeScript)                                  |
| TLS termination         | `TLSStore` / Let's Encrypt           | Automatic (managed)                                             |
| Custom business logic   | Go middleware plugins                | TypeScript policy modules                                       |
| Config management       | YAML files or K8s CRDs               | Git-native (`routes.oas.json` + `policies.json`)                |
| Documentation           | Separate tooling                     | Built-in developer portal (auto-generated from OpenAPI)         |

**Envoy → Zuplo**

| Concept                 | Envoy                          | Zuplo Equivalent                                                |
| :---------------------- | :----------------------------- | :-------------------------------------------------------------- |
| Route definition        | `route_config` in YAML         | Routes in `routes.oas.json` (OpenAPI format)                    |
| Upstream/backend        | `clusters`                     | `urlForwardHandler` with `baseUrl`                              |
| Rate limiting           | `envoy.filters.http.ratelimit` | `rate-limit-inbound` policy                                     |
| Authentication          | `ext_authz` filter             | `api-key-inbound`, `open-id-jwt-auth-inbound`, or custom policy |
| Request transformation  | Lua or Wasm filters            | Inbound policies (TypeScript)                                   |
| Response transformation | Lua or Wasm filters            | Outbound policies (TypeScript)                                  |
| TLS termination         | `transport_socket`             | Automatic (managed)                                             |
| Custom business logic   | C++/Wasm/Lua filters           | TypeScript policy modules                                       |
| Config management       | YAML + xDS API                 | Git-native (`routes.oas.json` + `policies.json`)                |
| Documentation           | Separate tooling               | Built-in developer portal (auto-generated from OpenAPI)         |

For a broader comparison of API gateway
[hosting models](/learning-center/api-gateway-hosting-options) and how
[different gateways compare](/learning-center/choosing-an-api-gateway), 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:

```json
{
  "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:

```json
{
  "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)

```yaml
# 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:

```json
{
  "/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:

```typescript
// 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:

```json
{
  "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](https://zuplo.com/docs/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:

```typescript
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?](/blog/2026/02/04/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](https://zuplo.com/docs/articles/custom-ci-cd) 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](https://zuplo.com/docs/articles/open-api) means you can
import your existing API specs to generate routes and a
[developer portal](https://zuplo.com/docs/articles/developer-portal) instantly.

If you're evaluating your options, check out our comparison of
[API gateway hosting options](/learning-center/api-gateway-hosting-options) and
the
[build vs. buy decision framework](/learning-center/build-vs-buy-api-management-tools)
to see how managed gateways stack up against self-hosted solutions. When you're
ready to start, [sign up for free](https://portal.zuplo.com/signup) and deploy
your first API to 300+ edge locations in minutes.