
The Digital Fast-Pass: Building a Low-Latency Global Application on AWS
A hands-on Terraform walkthrough of Route 53, CloudFront, Global Accelerator, ALB/NLB, WAF, Shield, and PrivateLink organized around three distinct traffic patterns, not one blended chain.
Users don't experience latency as an infrastructure metric. They experience it as waiting, and enough waiting eventually becomes abandonment.
Behind every fast global app is a network doing a lot of quiet, unglamorous work. The mistake most walkthroughs make (this one included, in an earlier draft) is treating that work as one long chain Route 53 to CloudFront to Global Accelerator to a load balancer to the database as if every service sits in a single request path. It doesn't. Real production architectures are usually three separate patterns stitched together, each solving a different problem:
- Static/cacheable content — skip the backend entirely.
- Dynamic global API traffic — get to the backend as fast as possible when you can't skip it.
- Private service-to-service traffic — never touch the public internet at all.
This post walks through each pattern with the Terraform to build it, not just the marketing description.
Pattern 1: Static/Cacheable
1
User → Route 53 → CloudFront → S3 / ALB origin
Route 53 resolves the hostname; CloudFront does the actual work. CloudFront's edge network currently spans 600+ Points of Presence and 13 regional edge caches across 100+ cities in 50 countries, so most requests never leave the edge:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
resource "aws_route53_record" "app_cdn" {
zone_id = var.hosted_zone_id
name = "cdn.example.com"
type = "A"
alias {
name = aws_cloudfront_distribution.app.domain_name
zone_id = aws_cloudfront_distribution.app.hosted_zone_id
evaluate_target_health = false
}
}
resource "aws_cloudfront_distribution" "app" {
enabled = true
origin {
domain_name = aws_lb.app_use1.dns_name
origin_id = "app-origin"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
target_origin_id = "app-origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
# NOTE: the ACM certificate for a CloudFront distribution must be
# requested in us-east-1, regardless of where your origin actually lives.
acm_certificate_arn = var.acm_cert_arn_us_east_1
ssl_support_method = "sni-only"
}
}
data "aws_cloudfront_cache_policy" "caching_optimized" {
name = "Managed-CachingOptimized"
}
1
2
3
[User Request] ──> [CloudFront edge] ──(cache hit, ~ms)──> [Response]
│
└─(cache miss)──> [Origin]
One warning worth repeating: it's tempting to extend this pattern to dynamic API responses using a custom cache policy. Be careful: cache-key design has to explicitly account for authorization and per-user request dimensions, or you can end up serving one user's response to another. Never cache authenticated or user-specific responses unless the cache policy is deliberately scoped for it.
Pattern 2: Dynamic Global API
1
User → Route 53 → Global Accelerator → ALB/NLB (per region) → ECS Fargate → Private RDS
This is the pattern for checkout flows, real-time bidding, multiplayer state sync, anything dynamic and uncacheable that still needs to find the nearest healthy region fast.
The important correction here: Route 53 doesn't do latency-based routing between your regions in this pattern; Global Accelerator does. Global Accelerator provides static anycast IPs and picks the optimal endpoint/region itself, based on its own routing logic and health checks. That's actually one of its main advantages over DNS-based routing: no dependency on DNS caching or TTLs. Route 53's job is simply to alias your hostname to Global Accelerator's static IPs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
resource "aws_globalaccelerator_accelerator" "app" {
name = "app-accelerator"
ip_address_type = "IPV4"
enabled = true
}
# Route 53 → Global Accelerator: fixed hosted zone ID for all accelerators
resource "aws_route53_record" "app_alias_ga" {
zone_id = var.hosted_zone_id
name = "app.example.com"
type = "A"
alias {
name = aws_globalaccelerator_accelerator.app.dns_name
zone_id = "Z2BJ6XQ5FK7U4H" # fixed value for Global Accelerator
evaluate_target_health = true
}
}
resource "aws_globalaccelerator_listener" "app" {
accelerator_arn = aws_globalaccelerator_accelerator.app.id
protocol = "TCP"
port_range {
from_port = 443
to_port = 443
}
}
resource "aws_globalaccelerator_endpoint_group" "use1" {
listener_arn = aws_globalaccelerator_listener.app.id
endpoint_group_region = "us-east-1"
endpoint_configuration {
endpoint_id = aws_lb.app_use1.arn
weight = 128
}
health_check_path = "/healthz"
health_check_interval_seconds = 10
threshold_count = 3 # consecutive checks before a state change — defaults to 3, made explicit here
}
resource "aws_globalaccelerator_endpoint_group" "euw1" {
listener_arn = aws_globalaccelerator_listener.app.id
endpoint_group_region = "eu-west-1"
endpoint_configuration {
endpoint_id = aws_lb.app_euw1.arn
weight = 128
}
health_check_path = "/healthz"
health_check_interval_seconds = 10
threshold_count = 3
}
On the performance claim: AWS advertises up to 60% improvement for Global Accelerator, and breaks that down in its own benchmarking further up to 49% lower p90 first-byte latency, up to 58% lower jitter, up to 60% higher throughput for TCP traffic, depending on the test. Treat "60%" as a ceiling, not an expectation; the actual gain depends heavily on your users' geography, last-mile conditions, protocol, and workload. Benchmark it against your own traffic before you put a number in a deck.
On failover: AWS describes Global Accelerator as redirecting traffic to a healthy endpoint within 30 seconds after it detects a failure, not a guaranteed fixed interval, and dependent on your
health_check_interval_seconds and threshold_count settings, both shown above.ALB vs. NLB, more precisely
| ALB | NLB | |
|---|---|---|
| Layer | 7 | 4 |
| Protocol awareness | HTTP/HTTPS-aware | TCP/UDP/TLS |
| Routing basis | Host, path, header | Connection/transport-level |
| Security integration | Native AWS WAF support | — |
| IP addressing | Dynamic | Static IP / Elastic IP per AZ |
| Best for | Request-level routing logic (checkout vs. browse) | Raw throughput, static IPs, PrivateLink targets |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
resource "aws_lb_listener_rule" "checkout" {
listener_arn = aws_lb_listener.app_https.arn
priority = 10
action {
type = "forward"
target_group_arn = aws_lb_target_group.checkout.arn
}
condition {
path_pattern {
values = ["/checkout/*"]
}
}
}
resource "aws_lb" "app_nlb" {
name = "app-nlb"
load_balancer_type = "network"
internal = false
subnets = var.public_subnet_ids
enable_cross_zone_load_balancing = true
}
A genuinely useful advanced pattern: put an NLB in front of an ALB (via IP targets) when you need NLB's static IPs or PrivateLink compatibility but still want ALB's L7 routing logic behind it:
1
2
3
4
5
6
7
Internet / PrivateLink
↓
NLB
↓
ALB
↓
Target Groups
The database connection: no PrivateLink needed
This is the correction that matters most: your ECS tasks should connect directly to RDS over private VPC networking, not through a PrivateLink interface endpoint.
1
2
3
4
ECS Fargate
│ private VPC networking (same VPC, private subnets)
▼
RDS PostgreSQL / MySQL
An interface VPC endpoint for
com.amazonaws.us-east-1.rds exists for the RDS control-plane API management operations like DescribeDBInstances or ModifyDBInstance not for your application's data-plane traffic to the database. When RDS lives in a private subnet, your app already reaches it over private VPC networking with no NAT gateway or internet gateway required. No PrivateLink resource belongs in this data path at all.1
2
3
4
5
6
7
8
resource "aws_security_group_rule" "ecs_to_rds" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.rds.id
source_security_group_id = aws_security_group.ecs_tasks.id
}
That's the entire "PrivateLink" of this pattern: a security group rule scoping traffic between two private resources in the same VPC.
Pattern 3: Private Service-to-Service
This is where PrivateLink actually belongs: exposing an internal service to other VPCs or accounts without putting it on the public internet; for example, an internal payments service consumed by several product teams' VPCs.
1
2
3
4
5
6
7
8
9
10
11
12
Consumer VPC
│
▼
Interface VPC Endpoint
│
AWS PrivateLink
│
▼
Provider VPC
│
▼
Internal Service / NLB
1
2
3
4
5
6
7
8
9
10
11
12
resource "aws_vpc_endpoint_service" "payments" {
acceptance_required = true
network_load_balancer_arns = [aws_lb.payments_internal_nlb.arn]
}
resource "aws_vpc_endpoint" "payments_consumer" {
vpc_id = var.consumer_vpc_id
service_name = aws_vpc_endpoint_service.payments.service_name
vpc_endpoint_type = "Interface"
subnet_ids = var.consumer_private_subnet_ids
security_group_ids = [aws_security_group.payments_endpoint.id]
}
The provider side puts an NLB in front of the internal service; the consumer side gets a private ENI in their own VPC that resolves to it. No route tables shared, no VPC peering, no public exposure.
Security: Shield, WAF, and Where Each One Attaches
Shield Standard is on by default for CloudFront and Route 53 at no extra cost, absorbing common network/transport-layer DDoS attempts at the edge. Shield Advanced adds 24/7 DDoS Response Team (DRT) access and cost protection for scaling charges incurred during an attack worth it for anything with real public exposure.
AWS WAF belongs in this stack too, but it attaches at a specific point, not automatically everywhere for Pattern 1 that's typically CloudFront, and for Pattern 2 that's typically the ALB:
1
2
Pattern 1: Route 53 → CloudFront → AWS WAF → S3/ALB origin
Pattern 2: Route 53 → Global Accelerator → ALB → AWS WAF (associated) → ECS
Global Accelerator itself doesn't carry WAF; the WAF web ACL attaches to the ALB (or CloudFront) behind it, not to the accelerator.
Putting It Together

Three patterns, three jobs:
- Pattern 1 skips the backend entirely for anything cacheable.
- Pattern 2 gets dynamic requests to the nearest healthy region as fast as possible, with the database reached over plain private VPC networking — not PrivateLink.
- Pattern 3 keeps internal service-to-service traffic off the public internet using PrivateLink, where it actually belongs.
The Bottom Line
In 2026, architecture is marketing. The teams that win are treating request path latency as a product metric, not an infra footnote measured, alerted on, and iterated against, the same way you'd treat conversion rate. Just make sure the architecture diagram you publish is one you could actually deploy.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article