AWS Builder Center
The Digital Fast-Pass: Building a Low-Latency Global Application on AWS

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.

AWS Community Builder - Dev Tools | 2× AWS Certified | Devops/Cloud Consultant
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:
  1. Static/cacheable content — skip the backend entirely.
  2. Dynamic global API traffic — get to the backend as fast as possible when you can't skip it.
  3. 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

ALBNLB
Layer74
Protocol awarenessHTTP/HTTPS-awareTCP/UDP/TLS
Routing basisHost, path, headerConnection/transport-level
Security integrationNative AWS WAF support—
IP addressingDynamicStatic IP / Elastic IP per AZ
Best forRequest-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.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article