
Serving 2-minute synchronous APIs behind Amazon API Gateway without changing a single client
API Gateway HTTP APIs stop waiting after 30 seconds. CloudFront path routing can send a few slow endpoints to an Application Load Balancer while everything else stays on API Gateway, with the same URL, the same JWT and safe retries.
The use case
A common setup: a public API sits behind an Amazon API Gateway HTTP API. Most calls finish in milliseconds. A handful don't. Bulk order creation, report generation and some partner integrations run synchronously for up to two minutes.
HTTP APIs have a hard 30-second integration timeout. Past that, the client gets a
504 , even though the backend carries on working and often finishes the job. The client then retries, and now there are duplicate records as well as an unhappy caller.Typical constraints in this situation:
- No client changes. Same hostname, same paths, same bearer token.
- Keep the existing identity provider. For example, Keycloak or any OIDC provider that issues RS256 JWTs.
- No immediate rewrite to async. A job-and-poll model is often the right long-term answer for some of these calls, but it isn't always possible in the short term.
This post walks through one pattern that meets those constraints, what a proof of concept showed, and where the pattern falls short.
The idea: let CloudFront pick the origin by path
The client can't know which calls are slow, and it shouldn't have to. Amazon CloudFront can route each request to a different origin based on its URL path. So a single CloudFront distribution goes in front of everything, with two origins:
Plain text
CloudFront (one hostname for everything)/ \slow paths / \ /v1/* (fast, < 30s)e.g. /v1/orders/bulk \▼ ▼Application Load Balancer API Gateway HTTP APIidle timeout 180s JWT authorizer, parameter mapping\ /▼ ▼Same backend service (for example, on Amazon ECS Fargate) ──▶ Database
- Fast calls (
/v1/*) keep going to API Gateway, with its JWT authorizer, throttling and parameter mapping unchanged.
- Slow calls match a more specific CloudFront behaviour that sits above
/v1/*and goes to an Application Load Balancer (ALB), whose idle timeout can be set as high as 4,000 seconds.
- Both origins reach the same backend service and return the same response.
From the client's point of view nothing changes.
POST /v1/orders/bulk still looks like any other /v1 call. It just doesn't time out any more.Behaviour order is the whole trick
CloudFront checks cache behaviours top to bottom, and the first match wins. The slow paths must come before the
/v1/* catch-all:Order | Path pattern | Origin |
|---|---|---|
1 | /long/* | ALB |
2 | /v1/orders/bulk | ALB |
3 | /v1/* | API Gateway |
default | * | ALB (static web page) |
A good practice is to keep the slow paths in one list in your infrastructure code. That list then drives the CloudFront behaviours, the ALB listener rules and the ALB's JWT rule, and a unit test can fail the build if a slow path ever ends up below
/v1/* . Moving another API to the long-running path becomes a one-line change plus a deploy.What CloudFront can't do
CloudFront can't predict how long a request will take, so you have to decide up front which APIs are slow.
- A whole path is always slow: one behaviour. Easy.
- A family of paths (
/v1/reports/*): one wildcard behaviour.
- Same path, but only one method is slow: behaviours match on path, not method. A CloudFront Function can change the origin per request, or you can send that whole path to the ALB.
- Same path and method, sometimes fast and sometimes slow: send it to the ALB. An ALB handles fast calls perfectly well.
A distribution allows 25 behaviours by default, so group slow APIs under patterns rather than adding one per endpoint.
Keeping authentication consistent on both paths
This is where most of the design effort goes. On the API Gateway path, the built-in JWT authorizer validates the token (signature,
iss , aud , expiry) and the backend never sees an invalid request. Parameter mapping then passes the token's sub to the backend as a header, overwriting any value the client sent.The ALB path needs the same guarantees. Two layers work well together:
- The backend checks the token itself on the slow paths. For example, Spring Security as an OAuth2 resource server, pointed at the same issuer and audience. It takes the user ID from the verified token, never from a header.
- The ALB's native JWT verification. ALB listener rules can validate a JWT against the identity provider's JWKS before forwarding, with no Lambda involved. Standard RS256 tokens from Keycloak work with it as they are.
The catch: ALB JWT verification only works on an HTTPS listener. Without a real domain, the ALB can only have a self-signed certificate, and CloudFront won't connect to an origin with one. In a quick proof of concept, CloudFront → ALB can stay on port 80 (locked to CloudFront's origin-facing prefix list), with the ALB's JWT check tested by calling its HTTPS listener directly. With a real domain and an ACM certificate, CloudFront connects to the ALB over HTTPS and both checks apply on the main path.
Also make sure that the internal backend paths API Gateway uses can't be reached through the ALB. The ALB should return
404 for them, and the backend's security group should only accept traffic from the ALB and the API Gateway VPC link. That matters, because those paths trust the user header set by API Gateway.One small gotcha: if the identity provider sits behind AWS WAF with a rule that blocks requests without a
User-Agent , API Gateway and the ALB may be blocked, because they fetch the JWKS without one. Relax that rule for the discovery and JWKS paths only.Timeouts: each layer waits longer than the one inside it
A 2-minute call only survives if every hop waits longer than the hop behind it:
Plain text
CloudFront 150s > ALB idle 180s ≥ backend cap 150sAPI Gateway HTTP API: 30s, fixed (the reason for this pattern)
Layer | Setting | Value |
|---|---|---|
CloudFront → ALB | origin response timeout | 60s by default; up to 180s after a free quota increase |
ALB | idle timeout | 180s |
ALB target group | deregistration delay | 180s, so running calls finish during deploys |
Backend | maximum for a slow call | 150s, with graceful shutdown |
Client | TCP keepalive | about every 15s |
Two lessons from testing:
- The CloudFront origin response timeout is 60 seconds by default. Anything longer needs a Service Quotas request. Plan for that early.
- Silent connections get dropped. No bytes flow while the backend works, and NATs and corporate firewalls between the client and AWS can drop an idle TCP connection well before two minutes. The ALB finishes the call, but the client never sees the answer. TCP keepalive on the client fixes it.
Making retries safe
Long calls get retried: by impatient users, by SDKs, by proxies. On the slow paths, an
Idempotency-Key header stored per user in an Amazon DynamoDB table with a one-day TTL makes retries safe:Same key sent again… | Result |
|---|---|
for the first time | runs, 201 |
while the first call is still running | 409 , nothing created |
after it finished | 200 with the original result, plus Idempotent-Replayed: true |
after the first call failed | key released, so the retry runs |
The claim is a DynamoDB conditional write, so two requests racing with the same key can't both win.
What a proof of concept showed
An end-to-end test of this pattern showed:
- CloudFront as the single entry point, with slow paths on the ALB and everything else on API Gateway.
- The same call failing at 30s through API Gateway and succeeding through the ALB.
- An existing
/v1path moved to the ALB with no client change.
- The ALB rejecting bad tokens with
401before the backend ever saw them.
- A 2-minute call completing with the ALB's JWT check.
- Retries with the same idempotency key creating no duplicates.
Two things need extra setup before they work: the full 2-minute call through CloudFront (needs the response-timeout quota increase) and the ALB's JWT check on the CloudFront path (needs a real domain and certificate).
Is this the right answer?
It's one option, not the only one, and not necessarily the default. Before building it, consider the simpler options:
- API Gateway REST API (Regional or private). REST APIs let you raise the integration timeout beyond 29 seconds through a quota request, and they support response streaming. A Lambda authorizer can validate the JWT. If REST API features and pricing fit, you keep one front door and avoid a second origin.
- Go async for the slowest calls. Return
202 Acceptedwith a job ID, and let the client poll or receive a callback. It's more work for clients, but it's the most robust option for anything that might grow past a few minutes.
- CloudFront → ALB / HTTP API (this post). Choose it when the fast majority of traffic needs to stay on HTTP APIs, clients can't change, and only a known, small set of paths is slow.
Whichever you choose, the same principles apply: decide the slow paths explicitly, make every timeout larger than the one inside it, validate the token on every path, and make long calls safe to retry.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article