/// tutorial · aws · 21 jul 2026

A low-latency application tier in Athens

Put the ALB and a small EC2 fleet in Athens, keep your data tier in Frankfurt over the backbone, and cut first-byte latency for Greek users from 91 ms to 24 ms. Here is the Terraform — and the full breakdown we measured.
/date
21 July 2026
/series
Athens Local Zone · part 2 of 6
/author
Thanasis Politis · Head of Solutions
/// scenario

You run an interactive service for Greek users — a checkout, a booking flow, a live dashboard, a media player. It works. But it lives in eu-central-1 (Frankfurt), and every request from Athens pays the same tax: roughly 44 ms round-trip on the wire before a single line of your code runs. One request, you barely notice. A real checkout is not one request — it validates the cart, looks up an address, reserves stock, creates a payment intent, confirms. A dozen sequential calls, and half a second is gone to network latency alone, on top of whatever the work actually takes.

The fix is not "make the app faster." It is "move the app closer." You take the synchronous, user-facing tier — an Application Load Balancer and a couple of app instances — and put it in the Athens Local Zone (eu-central-1-ath-1a), where it answers from about 11 ms away. The system of record — the database, the queues, the analytics — stays in Frankfurt, reached over the AWS backbone when the request genuinely needs it. Same VPC, same account, same Terraform. This part is the performance showcase of the series: we build the tier, then we measure it end to end.

Everything below is real. The stack ran in a live account and every number comes from measurement on 21 July 2026, not estimation. If you have not enabled the zone yet, start with part 1.
/// the_shape_of_it
A low-latency tier in AthensUser-facing compute in the Local Zone, system of record in the RegionAWS Region • eu-central-1 (Frankfurt)VPC • 172.31.0.0/16Availability Zone • eu-central-1asubnet 172.31.16.0/20Amazon RDSdata tier, stays in the RegionLocal Zone • eu-central-1-ath-1asubnet 172.31.48.0/24 • Athens2× EC2 c7i.largeapp fleetApplication LoadBalancerUser in Athensconsumer VDSLTTFB 24.3 ms34.3 ms to FrankfurtTTFB 90.6 ms if served from Frankfurt
The user-facing tier lives in Athens; the system of record stays in Frankfurt, one 34 ms backbone hop away. The dashed path is what every request costs today, before the Local Zone.
/// build_the_tier

Security groups first

Two groups: the load balancer accepts HTTP from the internet, and the app instances accept traffic only from the load balancer. Nothing reaches the instances directly. This is standard AWS hygiene and it works identically in a Local Zone.

security.tfterraform
resource "aws_security_group" "alb" {
  name        = "athens-alb"
  description = "Public ingress to the Athens ALB"
  vpc_id      = var.vpc_id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "athens-alb" }
}

resource "aws_security_group" "app" {
  name        = "athens-app"
  description = "App instances reachable only from the ALB"
  vpc_id      = var.vpc_id

  ingress {
    description     = "HTTP from the ALB only"
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "athens-app" }
}

The fleet: two c7i.large in the zone

Athens offers the C7i, M7i and R7i families — general-purpose, compute- and memory-optimised. For a stateless web/app tier, c7i.large is the natural default. We launch two so the target group has something to balance across, each running a trivial web server via user data. (Reusing the aws_subnet.athens Local Zone subnet from part 1.)

app.tfterraform
data "aws_ssm_parameter" "al2023" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}

resource "aws_instance" "app" {
  count                  = 2
  ami                    = data.aws_ssm_parameter.al2023.value
  instance_type          = "c7i.large"   # C7i / M7i / R7i only in Athens
  subnet_id              = aws_subnet.athens.id
  vpc_security_group_ids = [aws_security_group.app.id]
  user_data              = file("${path.module}/user_data.sh")
  tags = { Name = "athens-app-${count.index + 1}" }
}

The user data installs nginx and drops a /healthz file the target group will poll:

user_data.shbash
#!/bin/bash
set -euo pipefail
dnf install -y nginx

# health endpoint the target group checks
printf 'ok' > /usr/share/nginx/html/healthz

cat > /usr/share/nginx/html/index.html <<'HTML'
served from the Athens Local Zone (eu-central-1-ath-1a)
HTML

systemctl enable --now nginx

The load balancer, in a single Local Zone subnet

An Application Load Balancer usually spans two or more Availability Zones. In a Local Zone it runs entirely from the one Local Zone subnet, so both the load balancer and the targets it fronts sit in Athens and answer without ever touching Frankfurt. ALB is available in the Athens Local Zone — that is what makes this tier possible in-country.

alb.tfterraform
resource "aws_lb" "app" {
  name               = "athens-app-alb"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [aws_subnet.athens.id]   # one LZ subnet is allowed
  tags = { Name = "athens-app-alb" }
}

resource "aws_lb_target_group" "app" {
  name        = "athens-app-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "instance"

  health_check {
    path                = "/healthz"
    protocol            = "HTTP"
    matcher             = "200"
    interval            = 10
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 2
  }
}

resource "aws_lb_target_group_attachment" "app" {
  count            = 2
  target_group_arn = aws_lb_target_group.app.arn
  target_id        = aws_instance.app[count.index].id
  port             = 80
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.app.arn
  port              = 80
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

terraform apply, wait for the target group to report healthy, and the alb_dns_name output is a public endpoint served entirely from Athens. Point your DNS at it (or front it with CloudFront) and Greek traffic terminates in Greece.

/// measure_it

Now the payoff. We drove the ALB from a consumer VDSL line in the Athens metro and, for the same test, from the equivalent stack in Frankfurt. Start with the round-trip distribution — 100 pings, 0% loss:

round-trip (ms)Athens LZFrankfurt
minimum8.7342.1
median11.044.4
average11.744.52
p9515.7146.41
maximum41.750.2
jitter3.991.11

Read the honest bit too: Athens shows more jitter (3.99 ms vs 1.11 ms). At an 11 ms base, the last-mile VDSL variance is simply a larger share of the total; the longer Frankfurt path averages out to something steadier. It does not change the outcome — the p95 to Athens (15.7 ms) still sits below the fastest ping we ever saw to Frankfurt (42.1 ms). The slow case in Athens beats the best case in Frankfurt.

Latency is the point, so look at time to first byte over real HTTP (median of 12 requests). For small, interactive payloads — the shape of an API call — the first byte is what the user feels:

payloadAthens TTFBFrankfurt TTFBAthens totalFrankfurt total
1 KB24.3 ms90.6 ms24.4 ms90.7 ms
100 KB23.5 ms89.8 ms59.0 ms227.3 ms

First byte drops from 91 ms to 24 ms — a 3.7× cut on every single call. That gap is the TCP-plus-TLS handshake and the request itself each paying the round-trip once; shorter round-trip, cheaper handshake, faster everything. For larger transfers (media, bundles, downloads) the win shows up as throughput, because a fat pipe with a 34 ms delay carries less than the same pipe at 0.4 ms — the bandwidth-delay product working against you:

payloadAthens totalAthens throughputFrankfurt totalFrankfurt throughput
1 MB140.8 ms59.6 Mbit/s377.5 ms22.2 Mbit/s
10 MB899 ms93.3 Mbit/s1,631 ms51.7 Mbit/s

But the real story of an interactive tier is not one request — it is many, back to back. We ran the classic chatty pattern: 50 sequential 1 KB calls, the shape of a checkout or booking flow that cannot parallelise. From Athens it finished in 1.54 s. From Frankfurt, 6.04 s. Same code, same instance type — the 4.5 s difference is pure network, and it compounds with every extra round trip your flow makes.

50 sequential 1 KB API calls · wall-clock time (lower is better) Athens LZ 1.54 s 3.9× faster to complete Frankfurt 6.04 s
The chatty case is where a Local Zone earns its keep: latency you pay once per round trip, multiplied by every round trip.

One more layer, from inside AWS. We also measured on the private network between a c7i.large in Athens and one in Frankfurt — the exact link your app tier crosses when it does need the data tier:

metric (private network)in-zone (Athens ↔ Athens)cross-region (Athens ↔ Frankfurt)
round-trip latency (avg)0.38 ms34.3 ms
throughput (single stream)9,526 Mbit/s705 Mbit/s
throughput (8 streams)12,419 Mbit/s5,645 Mbit/s
HTTP first byte (10 MB object)0.6 ms69 ms

Two things fall out of that table. In-zone, the tier talks to itself in well under a millisecond, so a load balancer plus a fleet in Athens behaves like a single tightly-coupled box. Cross-region, the backbone is 34.3 ms and — crucially — this is the same number the consumer measurements imply: the fastest ping to Athens (8.73 ms) and to Frankfurt (42.1 ms) differ by ~33 ms, almost exactly the in-AWS Athens↔Frankfurt RTT. The physics is consistent end to end. The extra distance to Frankfurt is the entire story, and moving the tier removes it.

Does this apply to your workload?

The gain is proportional to how many round trips one user interaction costs, so the test is quick:

  • Move it if the path is synchronous and user-facing, chatty enough that a single interaction makes several calls, and your users are concentrated in Greece. Checkout, booking, live dashboards, interactive media.
  • Leave it in Frankfurt if the work is asynchronous, batched or scheduled. A nightly job does not care about 34 ms, and a queue consumer will never notice it.

Most estates have both. That is the point of the pattern — you move the tier that feels the distance, and nothing else changes.

/// cost_and_caveats

This is a small footprint: two c7i.large instances, one ALB, and its data processing, all at Local Zone rates (a modest premium over Frankfurt On-Demand, with Savings Plans and Spot available). For most interactive tiers that is rounding error next to the conversion you win by being fast. Enabling the zone itself is free.

The honest constraints, all of which shape the design above:

  • Keep the data tier in Frankfurt. RDS and ElastiCache are not in Athens. That is fine — the pattern here deliberately keeps the system of record in the parent Region and only moves the stateless, latency-sensitive front. Your synchronous user path stays local; the 34 ms backbone hop is paid only when a request genuinely needs the database.
  • Cache locally where it counts. With no managed cache in-zone, hot read paths that must not cross the backbone want an in-zone cache — run one on an instance in the fleet, or hold session/cart state at the edge — rather than a round trip to Frankfurt on every call.
  • Storage is single-zone. In-zone object storage is S3 One Zone-IA and block storage backups are EBS Local Snapshots — one zone of durability by design. Serve assets and scratch from here; keep the durable copy in Frankfurt. We cover in-country durability in a future article.
  • Compute is C7i, M7i and R7i. A web/app tier does not care; an inference tier stays in Frankfurt. We cover the full service and instance list in a future article.

None of this is a workaround. It is the right shape for a Local Zone: the tier that faces the user runs where the user is, and everything else runs where it already runs well.

github.com/Nexxion-ai/aws-athens-local-zone

The full Terraform for this tier — security groups, ALB, target group, health checks, the two instances, and the bench.sh harness that reproduces every number above — is in 02-low-latency-tier/ in the repo. Next in the series: in-country backup and recovery with EBS Local Snapshots and S3 One Zone-IA, and later, migrating an existing tier from Frankfurt. terraform destroy removes everything cleanly.

/// free_readiness_assessment

Book a free readiness assessment

In a short working session we help you understand what the Athens Local Zone means for your business. Together we identify:

  • Whether the Local Zone is relevant for your current architecture
  • Which workloads could benefit from the AWS Local Zone in Greece
  • Where data residency, latency, or DR requirements may create new opportunities
  • What the first migration or modernization step could look like

The goal is simple: decide whether the Athens AWS Local Zone matters for your environment — and what to do next.