Guide · Backend
API Gateway vs BFF: One Shared Front Door or One Backend for Each App
19 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- In an API gateway BFF setup, the API gateway is the one shared front door that every request passes through. A BFF (backend for frontend) is a small backend built for one client app.
- A gateway does the jobs every request needs. It routes (sends each request to the right service), checks the login token that proves who the user is, and caps requests per client. It also decrypts HTTPS traffic and logs each request, and knows nothing about screens.
- A BFF belongs to the team that builds one app. It calls several backend services and returns one response shaped for that app's screens, so a phone crosses its slow network once.
- A common layout uses both: the gateway in front, then one BFF per kind of user experience behind it. A small team with one website often needs only the gateway.
- Beginners can stop after the gateway vs BFF comparison table. After it, engineers get a gateway config tested on nginx 1.24 with curl checks, a BFF that survives a slow service, and the timeout order between layers.
What goes wrong when every app calls every backend service directly?
Every service has to repeat the same login checks, limits and logging, and every app has to know where every service lives. Phones also pay for each extra call over a slow mobile network.
Picture a common setup. A grocery delivery business has an Android app and a website for customers, plus a tablet app that partner shops use to pack orders. The setup is made up, but the problems are real.
Behind the apps sit eight backend services: catalogue, pricing, cart, orders, inventory, delivery slots, users and login. A service here is a small program that owns one job and its own data. The users service holds customer profiles, and the login service signs people in. In the first version, every app called the services directly.
Before: every app calls every service
Drag to exploreCount the arrows leaving the Android app: each is one or more trips over the phone network, and no box checks logins for everyone.
Diagram connections:
- Android app to Users service
- Android app to Catalogue service
- Android app to Pricing service
- Android app to Delivery slots service
- Android app to Orders service
- Android app to Inventory service
- Website to Catalogue service
- Website to Pricing service
- Shop tablet app to Orders service
- Shop tablet app to Inventory service
- Android app: Customers ordering groceries
- Website: Customers on laptops and search engines
- Shop tablet app: Shops see and pack orders
- Catalogue service: Products, photos, descriptions
- Pricing service: Prices and offers per shop
- Orders service: Places and tracks orders
- Inventory service: Stock count in each shop
- Delivery slots service: Free delivery times per area
- Users service: Customer names and addresses
The Android home screen made seven calls when it opened, and on a slow network it took about 2.4 seconds to fill (made up). The team kept a list of four other problems.
- Each service checked login tokens in its own way. The inventory service once forgot, and anyone could read stock levels.
- The website needs long product descriptions for search engines. The Android app downloaded them too, then threw them away.
- The tablet app asked the orders service for new orders every five seconds. One buggy tablet sent hundreds of requests a minute and nearly took the service down.
- When the team wanted to split the orders service in two, all three apps had to ship an update first, because each app knew the old address.
The rest of this guide fixes these problems one layer at a time. First comes an API gateway, then backends for frontends, and then the question of when one layer is enough.
What is an API gateway?
An API gateway is a server that sits in front of all your backend services and is the only address your apps call. It receives every request, applies shared rules, then forwards the request to the right service.
An API, short for application programming interface, is the set of requests a program accepts. For example, GET /v1/orders/981 asks the orders service for order 981. The gateway owns the public list of these requests, and the services sit privately behind it.
The microservices.io pattern page calls the gateway the single entry point for all clients. It also says the gateway insulates clients from how the application is split into services. So when the team splits the orders service, only the gateway's routing changes, and no app ships an update.
After: one API gateway in front
Drag to exploreEach app now has exactly one arrow. The token check happens in one place before any service sees the request.
Diagram connections:
- Android app to API gateway
- Website to API gateway
- Shop tablet app to API gateway
- API gateway to Login service (Verify token)
- API gateway to Catalogue service (/v1/catalogue)
- API gateway to Orders service (/v1/orders)
- API gateway to Inventory service (/v1/stock)
- Android app: Calls only the gateway
- Website: Calls only the gateway
- Shop tablet app: Calls only the gateway
- API gateway: Routes, checks tokens, limits, logs
- Login service: Issues and verifies login tokens
- Catalogue service: Products and descriptions
- Orders service: Places and tracks orders
- Inventory service: Stock count in each shop
What does an API gateway do for every request?
An API gateway does the cross-cutting work: jobs that every request needs, whichever service it is for. Doing them once at the door means no service has to remember them, as long as services accept traffic only from the gateway.
- Routing. It reads the request path, such as /v1/orders, and forwards the request to the matching service.
- Authentication. It checks the login token, a signed string the app received at login that proves who the user is. Missing or bad tokens stop here.
- Rate limiting. It caps how many requests one client may send in a time window. The buggy tablet now gets an error instead of hurting the orders service.
- TLS termination. TLS is the encryption behind HTTPS. The gateway decrypts incoming traffic, so the services behind it do not each manage public certificates.
- Logging and tracing. It records every request and stamps it with a request id, so one order can be followed through every service.
- Timeouts. It gives up on a service that stops answering and returns a clear error, instead of leaving the app waiting.
For rate limits, the usual reply is HTTP status 429, Too Many Requests. RFC 6585, the standard that defines it, says the reply may include a Retry-After header telling the client how long to wait. It leaves open how the server counts requests or identifies the user.
Envoy, a widely used open-source proxy, shows how much of this lives in proxies today. Its documentation lists routing by path, TLS termination, health checks, and global rate limiting through a separate rate limit service. It can also translate between HTTP/1.1 and HTTP/2.
How is an API gateway different from a load balancer?
A load balancer spreads traffic across several copies of the same server, while an API gateway decides which service a request is for and applies rules first. Most real systems use both.
Load balancers come in two broad kinds. A layer 4 balancer forwards connections using network addresses and ports, without reading the HTTP request inside. A layer 7 balancer reads HTTP, including the path and headers. The load balancer guide covers both in detail.
A gateway works at layer 7 by nature, because routing by path means reading the request. It also adds jobs a plain balancer usually skips, such as checking tokens and counting requests per client.
In the grocery setup the two layers stack. A load balancer spreads incoming traffic across three copies of the gateway. Each gateway copy then spreads requests across the copies of each service. If one gateway copy crashes, the balancer stops sending it traffic.
What is the backend-for-frontend (BFF) pattern?
A backend-for-frontend, or BFF, is a small backend built for one kind of client app and owned by the team that builds that app. It turns many service calls into the few responses that app's screens need.
The gateway fixed logins and limits, but not the slow home screen. The Android app still made seven calls. They just went through the gateway now.
So the platform team added a /home route to the gateway that called four services and merged the results. Then the shop team asked for a packing screen route, and the web team for a product page route with full descriptions.
Within three months the gateway config was 900 lines long, mostly screen logic (made up). Every Android change waited for a platform review. A typo in a website route once broke the tablet app's logins, because everything shipped as one gateway.
A travel agency with separate counters
Picture a travel agency. Everyone stops at one front desk, which checks your booking number. Behind it are counters for families, pilgrims and business travellers, and each counter's staff book flights, hotels and buses from the same back rooms.
| Travel agency | System | What carries over |
|---|---|---|
| Front desk | API gateway | One place everyone passes, checks who you are, knows nothing about your trip |
| Family, pilgrim and business counters | One BFF per kind of app | Each bundles what its own visitors need into one answer |
| Back rooms for flights, hotels, buses | Shared backend services | Every counter uses the same back rooms |
| Counter staff hired by that counter | BFF owned by the app's team | The counter changes its bundles without asking the front desk |
Where the picture breaks: a counter clerk visits back rooms one at a time, but a BFF calls its services at the same moment. And an agency has one front desk, while a gateway runs as many identical copies behind a load balancer.
Where the name came from
The pattern got its name at SoundCloud, the music streaming company. Phil Calçado, who worked there, wrote in September 2015 that their tech lead for web, Nick Fisher, coined the term. Rendering one profile page needed many calls to different endpoints, which hurt mobile users on slow networks.
Sam Newman's 2015 article on the pattern ties each BFF to one user experience. He passes on a guideline from Stewart Gleadow: one experience, one BFF. He adds that a BFF is typically maintained by the same team as its user interface, so the API and the screens change together.
A gateway and three BFFs
Drag to exploreOnly three of the services are shown. The gateway stays thin and shared, and each BFF, owned by its app's team, calls only what its app's screens need.
Diagram connections:
- Android app to API gateway
- Website to API gateway
- Shop tablet app to API gateway
- API gateway to Android BFF (/bff/android)
- API gateway to Web BFF (/bff/web)
- API gateway to Shop BFF (/bff/shop)
- Android BFF to Catalogue service
- Android BFF to Orders service
- Android BFF to Inventory service
- Web BFF to Catalogue service
- Shop BFF to Orders service
- Shop BFF to Inventory service
- Android app: Built by the Android team
- Website: Built by the web team
- Shop tablet app: Built by the shop tools team
- API gateway: Tokens, limits, TLS, logs; platform team
- Android BFF: Small replies for phones
- Web BFF: Full pages for search engines
- Shop BFF: Packing list and stock updates
- Catalogue service: Shared by all BFFs
- Orders service: Shared by all BFFs
- Inventory service: Shared by all BFFs
How do the two layers differ in an API gateway BFF setup?
An API gateway is one shared layer that applies the same rules to all traffic, while a BFF shapes data for one client app's screens. The gateway is about safety and routing. The BFF is about the user experience.
| API gateway | Backend-for-frontend (BFF) | |
|---|---|---|
| Main job | Route and protect every request | Build responses for one app's screens |
| How many | One shared layer, run as several copies | One per kind of user experience |
| Who owns it | The platform team | The app's own team |
| Knows about screens | No | Yes, that is its whole point |
| Typical logic | Tokens, limits, TLS, logs, timeouts | Combining calls, trimming fields, fallbacks |
| Changes | Rarely, with careful review | Often, with each app release |
| If it breaks | Every app is affected | Only its own app is affected |
| Grocery example | Turns away the buggy tablet's flood of requests | Home screen in one 40 KB reply instead of seven calls |
The microservices.io page treats the BFF as a variation of the gateway: a separate gateway for each kind of client. Sam Newman also describes a shared layer further upstream, such as a tier of nginx servers. This setup does the same with its thin shared gateway.
How do you build and check a small API gateway on nginx?
You can build a small API gateway from nginx, a widely used web server and reverse proxy. A reverse proxy is a server that passes requests on to other servers, and most gateways start as one.
Below is the team's gateway as a real nginx config. It was run on nginx 1.24.0, the Ubuntu 24.04 package, with small stand-in services on ports 9000 to 9003.
# gateway.conf: goes inside the http { } block of nginx.conf
limit_req_zone $binary_remote_addr zone=per_client:10m rate=2r/s;
server {
listen 8080;
limit_req zone=per_client burst=5 nodelay; # 2 per second, bursts of 5
limit_req_status 429; # Too Many Requests
proxy_connect_timeout 1s;
proxy_read_timeout 2s; # give up on a slow service
proxy_set_header X-Request-Id $request_id; # one id per request
add_header X-Request-Id $request_id always;
location /v1/catalogue/ { # browsing needs no login
proxy_pass http://127.0.0.1:9001;
}
location /v1/orders/ {
auth_request /check-login; # refused unless login check returns 2xx
proxy_pass http://127.0.0.1:9002;
}
location /v1/stock/ {
auth_request /check-login;
proxy_pass http://127.0.0.1:9003;
}
location = /check-login {
internal;
proxy_pass http://127.0.0.1:9000/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Request-Id $request_id; # not inherited here
}
}Four parts do the work. The catalogue route has no auth_request line, so customers can browse before logging in.
The orders and stock routes send a small internal request to the login service first. Then nginx refuses the request unless that check returns a 2xx status, any success code from 200 to 299. A 401 or 403 from the login service is passed on to the app; any other answer, or no answer, becomes a 500.
The limit is counted per client IP address and allows a burst of 5 on top of 2 requests a second. To limit per user instead, the key must come from the token. Without limit_req_status, nginx rejects with 503, not 429.
Behind a load balancer, every request arrives from the balancer's address, so all users share one count. In a test with two clients behind one address, the second client got only 429s.
The realip module fixes this: set_real_ip_from marks the balancer's address as trusted, and real_ip_header X-Forwarded-For reads the client address from that header. With it, both test clients got their full burst. Like auth_request, it is left out of a plain source build unless you add --with-http_realip_module, and Ubuntu's 1.24.0 package includes it.
Each gateway copy also counts on its own, so three copies allow up to three times the limit. For one shared count, use a separate rate limit service, as Envoy's global rate limiting does.
proxy_read_timeout 2s gives up when a service sends nothing for 2 seconds, measured between two reads, not over the whole reply. Finally, $request_id is a fresh 32-character hex id for each request, sent to the service, the login check and back to the app. The login check needs its own X-Request-Id line, because nginx drops inherited proxy_set_header lines from any level that sets its own.
To stay short, this test listens on plain HTTP port 8080. A real gateway also terminates TLS on port 443 with a certificate, which this test leaves out.
How do you check the gateway works?
Send a few requests with curl and compare the status codes. These are the results from the test run. The stand-in login service accepts one fake token, test-token, where a real one would check the token's signature and expiry.
# browse without a token
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/v1/catalogue/tomatoes # 200
# orders without a token, then with one
curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/v1/orders/981 # 401
curl -s -o /dev/null -w '%{http_code}\n' -H 'Authorization: Bearer test-token' localhost:8080/v1/orders/981 # 200
sleep 3 # let the limit reset after the calls above
# ten quick requests: the first 6 pass, then the limit starts
for i in $(seq 1 10); do curl -s -o /dev/null -w '%{http_code} ' localhost:8080/v1/catalogue/; done; echo
# 200 200 200 200 200 200 429 429 429 429
sleep 3
# a service that takes 3 seconds to answer
curl -s -o /dev/null -w '%{http_code} after %{time_total} s\n' -H 'Authorization: Bearer test-token' localhost:8080/v1/orders/slow
# 504 after 2.008766 sSix requests pass, not two, because the burst of 5 is allowed on top of the first one. The slow call ends with 504 Gateway Timeout just after 2 seconds, which is the read timeout at work.
Two more details showed up in the test. The limit runs before the login check, so a flood without tokens gets 429, not 401. And the 429 carried no Retry-After header, so add one yourself if your apps should know when to retry.
How does a BFF combine several calls into one screen?
A BFF receives one request from its app, calls the needed services at the same time over the fast private network, and returns one trimmed response. The slow phone network is crossed once instead of seven times.
One Android home screen request
Drag to exploreOne trip over the phone network instead of seven; about 0.9 seconds instead of 2.4 (made up). The four service calls happen inside the data centre.
Diagram connections:
- Open the home screen to Check limit, then token
- Check limit, then token to Forward to Android BFF (Token valid)
- Forward to Android BFF to Call four services at once
- Call four services at once to Show the screen (One trimmed 40 KB reply)
- Open the home screen: Android app → Gateway
- Check limit, then token: Gateway → Login service
- Forward to Android BFF: Gateway → Android BFF
- Call four services at once: Android BFF → users, pricing, slots, inventory
- Show the screen: Android BFF → Android app
Here is the heart of the Android BFF in Python, written so you can run it. Each call function stands in for one service call and just waits for a set time. Each call is async, which means the program can wait on several calls at once.
import asyncio
import time
async def call(seconds, result):
await asyncio.sleep(seconds) # stands in for one service call
return result
async def home_screen():
profile, offers, slots, low_stock = await asyncio.gather(
call(0.10, {"first_name": "Guest"}), # users service
call(0.20, ["tomatoes 10% off", "onions 5% off"]), # pricing
call(0.15, ["7-8 pm", "8-9 pm"]), # delivery slots
asyncio.wait_for(call(2.0, ["milk"]), timeout=0.3), # slow stock
return_exceptions=True,
)
if isinstance(profile, Exception):
raise profile # no profile, no screen
def safe(part, limit):
return [] if isinstance(part, Exception) else part[:limit]
return {"name": profile["first_name"],
"offers": safe(offers, 6),
"slots": safe(slots, 3),
"low_stock": safe(low_stock, 5)}
start = time.perf_counter()
print(asyncio.run(home_screen()))
print(f"took {time.perf_counter() - start:.2f} s")Running it with Python 3.13 printed the screen with an empty low_stock list and took 0.30 s in all 10 recorded runs. Awaiting the same four calls one after another, with the same cap, took about 0.75 s.
What just happened?
First, asyncio.gather starts all four calls together, so the wait is the slowest call, not the sum. Second, return_exceptions=True hands back a failure as a value in the result list instead of raising it.
Third, wait_for gave the stock call only 0.3 seconds. When that ran out, it cancelled the slow call and raised TimeoutError, which gather put in the result list. The safe helper turned it into an empty list. Only a missing profile stops the screen.
Without return_exceptions=True, the first error is raised straight to the BFF. The Python docs warn that the other calls are not cancelled then and keep running, so clean them up yourself.
Sam Newman makes the same point about failure. When only one less important service is down, he suggests degrading what the BFF returns. The app must then handle a partly filled response without crashing.
When do you need a BFF, and when is one gateway enough?
You need a BFF when different apps need very different data, or when app teams are slowed down waiting on a shared backend. One gateway is enough when you have one app, or apps that want the same data.
Sam Newman suspects a BFF only makes sense for a web app when a significant amount of server-side combining is needed. For mobile apps or outside partners, he would strongly consider one BFF per party from the start.
Signs that one gateway is enough:
- You have one client app, or several that show nearly the same screens.
- One small team builds both the apps and the backend, so nobody waits on another team.
- Most screens need data from one or two services, so there is little to combine.
Signs that a BFF will pay for itself:
- Phone screens need a fraction of the data the website needs.
- One screen needs four or more services, and users are often on slow networks.
- App teams wait days for platform reviews to change an endpoint.
The grocery team has a new question. The Android team will build an iPhone app next year. Should it get its own BFF?
Sam Newman reports that SoundCloud's Android and iPhone listener apps shared one BFF, though its engineers might reconsider that today. He is more relaxed about sharing when one team owns both apps and the BFF. Here one team owns both, and the screens match, so they plan one shared mobile BFF.
What goes wrong with API gateways and BFFs?
Most problems come from putting logic in the wrong layer or copying logic between BFFs. Every layer also adds a hop, one more network stop between machines, and a place to fail. Each problem has a simple guard.
- The gateway becomes a single point of failure. If it goes down, every app goes down. Guard: run several copies behind a load balancer, spread across data centres.
- The gateway fills with screen logic, as the 900-line config did. Guard: the any-request test from earlier, and moving screen routes into BFFs.
- BFFs copy the same logic. Guard: before you write the same code a third time, move it into a proper service.
- Timeouts fight each other. Guard: each inner layer gives up sooner than the layer outside it.
- A BFF reads service databases directly to save time. Guard: BFFs call service APIs only, so services can change their storage freely.
Copying is the trap SoundCloud hit. Phil Calçado describes duplicated profile-fetching code across all of their roughly five BFFs. The team read this as a missing object in their domain model, the set of core things the business works with. So they built a user profile service.
Sam Newman suggests the old rule of thumb behind the guard above. Build the shared piece when you are about to write the same thing for the third time.
Why timeouts must shrink inward
If the gateway gives up after 2 seconds but the service behind it takes 3, the service keeps working for nobody. The test showed exactly this: the app got its 504 at 2 seconds, and the slow service only found out a second later, when its reply had nowhere to go.
So set them in order. If the gateway waits 2 seconds, the BFF should give each service well under that, like the 0.3 seconds for stock, and leave time to build the reply.
How much does the extra hop cost?
The microservices.io page lists the extra network hop as a drawback, but says that for most applications its cost is insignificant. You can measure your own with curl, calling a service directly and then through the gateway.
curl -s -o /dev/null -w '%{time_total}\n' localhost:9001/v1/catalogue/x # direct
curl -s -o /dev/null -w '%{time_total}\n' localhost:8080/v1/catalogue/x # through the gatewayOn one shared test machine, two runs of 20 requests each showed nginx adding a median of 0.6 and 2.0 milliseconds. Single requests varied far more, so compare medians, not one call.
Both are same-machine figures. A real hop also crosses the network between machines, so measure in your own data centre before you trust any number.
There is one more question: should the gateway or the service check logins? The answer is both, at different depths. The gateway checks that the token is valid. The orders service still checks that this user owns order 981.
How do you explain API gateway and BFF in a system design interview?
Explain them as two layers with different jobs: the gateway protects and routes all traffic, and each BFF shapes data for one app. Then justify each layer with a problem it solves.
Start with the clients
Name every kind of client: Android, website, shop tablet, partners. Ask how different their screens and networks are.
Add the gateway
Draw one gateway in front of the services. List its jobs: routing, token checks, rate limits, TLS termination, logs and timeouts.
Put a balancer in front
Show the gateway as several copies behind a load balancer, so it is not a single point of failure.
Add BFFs only where needed
Give a BFF to each user experience whose screens need combined or trimmed data. Say which team owns each one.
Walk through one screen
Trace one request end to end. Show parallel calls in the BFF, the optional parts, and their fallbacks.
Name the risks
Mention copied logic between BFFs, timeouts that shrink inward, and how you would measure the extra hop.
Questions people ask
- Is a BFF the same as an API gateway?
- No, but they are related. A gateway is one shared layer that routes and protects all traffic. A BFF is one backend per kind of client that shapes data for that client's screens. Some writers describe the BFF as a variation of the gateway pattern, with one gateway per client type.
- Can an API gateway combine calls from several services?
- Yes. Calling several services and merging the results is called API composition, and microservices.io notes that an API gateway often does it. It works well for one or two simple cases. Once the merging is shaped around particular screens, it usually belongs in a BFF, so the app team can change it without touching the shared gateway.
- Does GraphQL replace the need for a BFF?
- Sometimes it covers the same need. GraphQL is a query language that lets an app ask for exactly the fields it wants, across related data, in one request. That removes much of the trimming and combining a BFF does. You still have to decide who owns the GraphQL layer, and screen-specific logic can still pile up there.
- Should authentication happen in the gateway or the BFF?
- Check the login token in the gateway, so no request reaches a BFF or service without a valid one. Then each service still checks permissions for its own data, such as whether this user owns this order. The gateway answers who you are, and the service answers what you may touch.
- How many BFFs should a company have?
- One per distinct user experience, not one per device. A popular rule of thumb is one experience, one BFF. If the iPhone and Android apps show the same screens and one team owns both, a shared mobile BFF is fine. Separate teams or very different screens usually justify separate BFFs.
- Is an API gateway a single point of failure?
- It can be, if you run only one copy. Every request passes through it, so an outage stops every app. Run several copies behind a load balancer, spread them across data centres, and keep the gateway's own logic small so it rarely needs risky changes.
- Do I need an API gateway for a monolith?
- Often not at first. A monolith, one program holding all the features, can sit behind a plain load balancer or a reverse proxy, a server that passes requests on to it. A gateway starts to help when you split the monolith, because it can send some routes to new services while the rest still go to the old program.
Sources and further reading
- Pattern: API Gateway / Backends for Frontends (Chris Richardson, microservices.io)
- Pattern: API Composition (Chris Richardson, microservices.io)
- Backends For Frontends (Sam Newman, 18 November 2015)
- The Back-end for Front-end Pattern (BFF) (Phil Calçado, 18 September 2015)
- What is Envoy (Envoy proxy documentation)
- RFC 6585: Additional HTTP Status Codes, section 4: 429 Too Many Requests (IETF)
- nginx docs: ngx_http_auth_request_module
- nginx docs: ngx_http_limit_req_module (limit_req, limit_req_status)
- nginx docs: ngx_http_realip_module (set_real_ip_from, real_ip_header)
- nginx docs: ngx_http_proxy_module (proxy_read_timeout, proxy_set_header)
- nginx docs: ngx_http_core_module ($request_id, internal)
- Python docs: Coroutines and Tasks (asyncio.gather, asyncio.wait_for)
- GraphQL home page (graphql.org)