What problem the API gateway pattern solves
In a microservice architecture, the operations a client cares about are usually spread across many fine-grained services. If a mobile app renders an order screen by calling the order, customer, inventory and shipping services directly, three problems follow.
Round trips multiply. Each screen needs several network calls, often over a slow or high-latency link. The microservice architecture pattern lists "efficient interactions" as a force for this reason: many round trips and large transfers make a distributed operation too slow.
Clients couple to internal boundaries. If you split a service in two or merge two, every client that calls them must change. You can't redeploy a mobile app the way you redeploy a backend, so the internal design gets frozen by whichever app versions are still installed.
Cross-cutting concerns get duplicated. Every public-facing service has to terminate TLS, validate tokens and enforce rate limits. Each team implements these slightly differently, and the result is inconsistent security.
The API gateway pattern addresses all three with one thin layer at the system boundary. Clients talk only to the gateway, and the gateway routes requests to the services behind it:
[Web] [Mobile] [Partner]
\ | /
+------+-------+
|
+---------------+ <- system boundary
| API Gateway |
+---------------+
| | |
Orders Customers Inventory
The gateway's route map should describe the operations you expose externally, such as GET /orders/{id}. It should not mirror the internal service list one-to-one. If it does, the gateway passes the internal coupling straight through to clients and none of the three problems are solved.
+---------+ +---------+ +-----------+
| Web | | Mobile | | Partner |
+---------+ +---------+ +-----------+
| | |
+------------------+------------------+
| one request per client operation
v
- - - - - - - - - - - - system boundary - - - - - - - - - - - -
+--------------------------------------------------------+
| API Gateway |
| TLS termination | token validation | rate limiting |
+--------------------------------------------------------+
| Route map = external operations, not internal services|
| GET /orders/{id} -> Orders + Customers + Shipping |
| POST /orders -> Orders + Inventory |
| GET /stock/{sku} -> Inventory |
+--------------------------------------------------------+
| | | |
v v v v
+----------+ +-----------+ +-----------+ +----------+
| Orders | | Customers | | Inventory | | Shipping |
+----------+ +-----------+ +-----------+ +----------+
internal services: can be split or merged without
changing clients, as long as the route map holds
Clients call a single API gateway at the system boundary, which handles TLS, token validation and rate limiting once and maps externally exposed operations onto internal services without exposing their boundaries.
How an API gateway works: responsibilities that belong at the edge
A request reaching the gateway passes through an ordered chain. First, TLS is terminated so the gateway can inspect the request. Next, edge routing matches the host and path, sending api.example.com/orders/* to the order service, for example. Then policies run: authentication, rate limiting, and size checks. Only after all of these does the request reach an upstream. The order matters because cheap rejections should happen before expensive work. A request with an oversized body or an exhausted quota should never trigger a token introspection call or open an upstream connection.
Concerns the gateway owns
- Edge routing by path and host, so internal topology can change without clients noticing.
- TLS termination in one place, which centralizes certificate rotation.
- Authentication and coarse authorization, such as "is this token valid and does it carry the
orders:readscope?" - Rate limits and quotas per client or API key, which protect shared backends from a single noisy consumer.
- Request and response size limits, which bound memory use and reduce abuse.
- CORS, handled once instead of being reimplemented differently in each service.
- Request IDs, structured logs and trace context, generated or propagated at the edge so every downstream hop shares one correlation key.
Identity flow
The gateway validates the token and forwards verified claims, such as the subject and scopes, in a header that downstream services trust only from the gateway. Services must still enforce fine-grained authorization themselves, for example "can this user read this order?" Internal callers, batch jobs and misconfigured network paths can all reach a service without going through the gateway.
A typical declarative route config describes each route as a path prefix, an upstream, a required auth scheme, a rate limit, and a per-route timeout. The per-route timeout matters: a slow report endpoint should not dictate the timeout for a fast lookup.
# Product-neutral sketch: not the schema of any specific gateway.
# Policies run in listed order so cheap rejections happen first.
listeners:
- host: api.example.com
tls:
certificate: /etc/gateway/certs/api.example.com.pem # TLS terminates here
defaults:
request_id_header: X-Request-ID # generate if missing, propagate downstream
propagate_trace_context: true # forward W3C traceparent to upstreams
max_request_body: 1MiB # reject oversized bodies before any auth work
routes:
- match:
path_prefix: /orders/
upstream:
policies:
- rate_limit:
key: api_key # quota per client, not per IP
requests_per_minute: 600
- authenticate:
type: jwt
issuer:
audience: orders-api
required_scopes: [orders:read] # coarse check only
forward_claims_header: X-Verified-Claims # strip any client-sent copy
timeouts:
connect: 1s
request: 5s # fail fast instead of tying up connections
- match:
path_prefix: /reports/
upstream:
policies:
- rate_limit: { key: api_key, requests_per_minute: 60 }
- authenticate: { type: jwt, required_scopes: [reports:read] }
timeouts:
request: 30s # slower route gets its own budget
A product-neutral declarative gateway config showing host and path routing, TLS termination, a body size limit, per-client rate limits, JWT authentication with scope checks and claim forwarding, and per-route timeouts.
Request aggregation and the backend for frontend variant
Sometimes one screen needs data that lives in several services. An order detail page might need the order, the customer's profile and shipment tracking. If the client calls each service itself, a mobile app on a high-latency network pays for several round trips. A gateway can fan out to all three in parallel over the data-center network and return one merged response. That is a good trade when the calls are independent, the merge is simple, and most clients need the result.
The cost is coupling. The aggregated response can't be faster than its slowest dependency, so a slow tracking service makes the whole page slow. You also have to decide up front what happens when one call fails. Give each downstream call its own timeout. Mark which fields are optional so the gateway can return a partial response, such as the order without tracking. Define fallbacks, such as cached or empty values, and how the client learns that it received them. If you add these after the first incident, clients will already rely on all-or-nothing behavior.
As client types diverge, the shared gateway tends to fill up with conditionals for each client. The backend for frontend (BFF) variant splits it: one gateway each for web, mobile and third-party consumers, each owned by the team that builds that client. Each team shapes payloads and changes its BFF without coordinating with the others.
The downside is duplication. Auth checks, error mapping and pagination conventions get copied into each BFF, and the copies drift apart over time. Keep cross-cutting concerns in a thin common edge layer or in the services, and keep only presentation logic in the BFFs.
API gateway vs reverse proxy, load balancer and service mesh
These four components are often confused because each one sits in a request path and forwards traffic. The useful distinctions are which direction the traffic flows and which team owns the policy.
| Component | Traffic direction | Typical concerns | Usual owner | Choose it when |
|---|---|---|---|---|
| Reverse proxy / load balancer | North-south, sometimes internal | TLS termination, health checks, distributing connections | Infrastructure team | You need availability and routing, but no API-level policy |
| API gateway | North-south | Authentication, rate limits, routing by path or version, request shaping | Platform team | Several services are exposed to external clients under one contract |
| Backend for frontend | North-south, per client | Aggregation and payload shaping for one UI | The client's product team | Client needs diverge enough to justify a separate edge |
| Service mesh | East-west | mTLS between services, retries, timeouts, telemetry | Platform team | Many services call each other, and you want uniform behaviour without library code |
The gateway and the mesh overlap because both can retry requests, encrypt with mTLS and emit telemetry. The difference is the trust boundary. A gateway deals with clients it does not control. It validates their credentials and enforces quotas tied to API consumers. A mesh assumes every caller is a workload with a known identity, and it applies policy on each hop between services. Removing the gateway leaves no place to enforce consumer-facing contracts. Removing the mesh pushes service-to-service security and resilience back into application code.
For a system with one or two services and a single client, direct client-to-service calls are still a reasonable choice. Each layer above adds a network hop and another component to deploy and debug.
When you need an API gateway and when to avoid one
Use the following checklist. If two or more items apply, a gateway usually costs less than the duplication it removes.
| Signal | Points toward a gateway | Points away from one |
|---|---|---|
| Clients | Web, mobile and partner clients with different payload and latency needs | One client, usually a single web app |
| Externally exposed services | Several services reachable from outside | One service, or a monolith |
| Cross-cutting concerns | Auth, rate limiting and TLS reimplemented in each service | Handled once, in one codebase |
| Public contract vs internal boundaries | Public API must stay stable while services are split or merged | Public API maps directly onto one deployable |
| Team ownership | Many teams, each owning services behind a shared entry point | One team owns the whole stack |
The contract signal is often the strongest. Without a gateway, the way you split services leaks into client URLs. Every refactor then becomes a breaking change for mobile apps you cannot force users to update.
When to skip it
A single service, or a monolith serving one client, does not need a gateway. A reverse proxy handles TLS termination, compression and path routing. A dedicated gateway would add a hop, a deployment and an on-call surface without solving a real problem.
The failure mode to watch
Keep business logic, data transformation and multi-service orchestration out of the gateway. Each of these feels convenient the first time someone adds it. Over time, the gateway becomes a shared codebase that every team must change and redeploy to ship features. That rebuilds the coupled monolith the service split was meant to remove, and it now sits on the critical path for every request.
Implementing and operating a gateway safely
Because every external request passes through the gateway, run it as a stateless, horizontally scaled tier: at least two instances spread across failure zones behind a load balancer, with no session state held locally. That way an instance can be lost or replaced without dropping traffic. Size it for peak load with headroom, because when the gateway saturates, every service behind it fails at the same moment.
Treat gateway configuration as code. Keep routes, auth rules and limits in version control, require review, and run automated tests that check routing and policy before a merge. Deploy changes in stages, to one instance or a small share of traffic first, so a bad route or broken auth rule reaches a few users rather than all of them.
Set resilience behavior explicitly for each route:
- Timeouts. Base each one on that backend's real latency. A single global value is too long for fast routes and too short for slow ones.
- Retries. Limit them with a budget, and agree with service and client owners on which layer retries. If client, gateway and service each retry three times, one failed call can turn into many backend requests during an incident. That load can keep a struggling service from recovering.
- Circuit breaking. Stop sending traffic to a backend that keeps failing, and let it recover.
For example, a route might use these settings (illustrative values, not recommendations):
| Setting | Value |
|---|---|
| Timeout | 2 seconds |
| Retries | 1, only on idempotent methods |
| Retry budget | Retries capped at 20% of active requests |
| Circuit breaker | Opens after 5 consecutive errors, ejects the failing instance for 30 seconds |
Expose metrics for each route: request rate, error rate, latency percentiles, retry counts and breaker state. Aggregate gateway metrics can look normal while a single route fails, so per-route data is what lets you see which backend is in trouble.
# Illustrative, product-neutral gateway route config.
# Field names show the concepts, not any specific gateway's schema.
# All values are examples, not recommendations.
routes:
- name: orders-read
match:
path_prefix: /orders
methods: [GET, HEAD] # idempotent only, so retries are safe
backend: orders-service
timeout: 2s # from this backend's measured latency, not a global default
retries:
attempts: 1 # one retry at most; services and clients agree not to retry too
retry_on: [connect-failure, 503]
per_try_timeout: 1s # keeps the retry inside the 2s overall timeout
- name: orders-write
match:
path_prefix: /orders
methods: [POST]
backend: orders-service
timeout: 3s
retries:
attempts: 0 # never retry non-idempotent writes at the gateway
backends:
orders-service:
retry_budget:
max_retry_ratio: 0.20 # retries capped at 20% of active requests
min_concurrent_retries: 3 # allows a few retries when traffic is very low
circuit_breaker:
consecutive_errors: 5 # eject an instance after 5 consecutive errors
ejection_time: 30s # keep it out of rotation for 30 seconds
max_ejected_percent: 50 # always leave some instances serving traffic
metrics:
per_route: true # rate, errors, latency percentiles, retries, breaker state
labels: [route, backend, status_class]
A product-neutral gateway configuration with illustrative values. Each route sets its own timeout, retries are limited to idempotent methods and capped by a backend retry budget, and circuit breaking ejects instances that keep failing.
Impact on development cost and engineering time
The gateway adds three kinds of cost. Every request takes an extra network hop, plus whatever processing the gateway performs: TLS termination, token validation and policy checks. That latency applies to every call, including calls that need none of those features. The gateway is also another production system with its own capacity planning, upgrades and on-call rotation, and because all traffic flows through it, a gateway outage takes down everything. The organizational cost is easier to miss. If one platform team must approve every route or header change, the gateway becomes a queue that every product team waits in.
The savings come from consolidation. Authentication, TLS, rate limiting and request telemetry are built and patched in one place instead of in every service. The public contract is separate from internal topology, so you can split, merge or rename services without breaking clients. Client teams integrate faster because they work with one base URL, one auth scheme and one error format.
Illustrative estimate (assumed numbers, not a benchmark): Annual net hours saved = S × H − G, where S is the number of services, H is the yearly hours each service would spend on edge concerns, and G is the yearly hours spent operating the gateway.
- With S = 12, H = 40 and G = 300, the result is 480 − 300 = 180 hours saved.
- With S = 4, the result is 160 − 300 = −140 hours, a net loss.
To remove the bottleneck, let teams own their route definitions in their own repositories, with CI validating them against platform policies. Backends for frontends move client-specific aggregation to the teams that own each client. Both approaches add components to operate: a policy pipeline in the first case, and one more deployable service per client type in the second.
How OneEnv can help
Disclosure: OneEnv is the product behind this blog.
OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.
Applied to api gateway pattern, this is where it helps:
Saves time
- Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
- One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
- Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.
Saves cost
- Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
- Less duplicated configuration to maintain: one shared definition inherited by many services.
- Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
- OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.
These are qualitative benefits, not measured results — how much you save depends on how many services share the value.
Want to try it? Start at oneenv.io.