Every 10,000 km between your server and your user adds ~100ms of baseline latency. A Shopify app in US-East serving Singapore adds 300-400ms before your code runs.
Here are the five architectural decisions that determine whether your Shopify system serves the world or just one AWS region.
Pick the Right Topology First
Don't start with active-active. Most teams underestimate the operational complexity.
Topology
RTO
RPO
When to Use
Active-Passive
2–10 min
< 30s
Default: resilience without write conflicts
Active-Active (2)
< 1 min
Near-zero
US + EU merchant split, compliance-driven
Active-Active (3+)
Seconds
Near-zero
Shopify Plus global stores
Edge-Only (Oxygen)
Built-in
N/A
Hydrogen storefronts
Start active-passive. Upgrade when traffic or compliance forces your hand.
GeoDNS with Health Check Failover
Route 53 latency-based routing is the cleanest solution for AWS-hosted Shopify apps. evaluate_target_health = true is the critical setting — without it, DNS failover doesn't trigger on regional failure.
hclresource "aws_route53_record" "shopify_app_eu" {
zone_id = aws_route53_zone.main.zone_id
name = "api.yourshopifyapp.com"
type = "A"
set_identifier = "eu-west-1"
latency_routing_policy {
region = "eu-west-1"
}
alias {
name = aws_lb.eu_west.dns_name
zone_id = aws_lb.eu_west.zone_id
evaluate_target_health = true # Fails over on health check failure
}
}
For Cloudflare users, geo-steering with origin pools per continent achieves the same result without Route 53.
- Cross-Region Webhook Deduplication
Shopify doesn't know your regional topology. The same webhook hits both your US and EU endpoints simultaneously when a global load balancer fronts both regions.
js// Upstash Global Redis: replicated across all regions
const acquired = await globalRedis.set(
`webhook:dedup:${webhookId}`,
process.env.DEPLOY_REGION,
{ nx: true, ex: 86400 }
);
if (!acquired) {
// Another region claimed this webhook — skip
return { status: 'duplicate' };
}
// This region won — process the webhook
await enqueueWebhookJob(topic, shop, payload);
SET NX is atomic. Two regions racing on the same key cannot both proceed. The 24-hour TTL handles Shopify's retry window.
- GDPR Data Residency Routing
EU merchant data cannot touch US infrastructure for storage or processing. Assign each merchant to a region at installation:
jsasync function assignMerchantRegion(shop, shopifyShopData) {
const EU_COUNTRIES = new Set([
'AT','BE','BG','CY','CZ','DE','DK','EE','ES','FI',
'FR','GR','HR','HU','IE','IT','LT','LU','LV','MT',
'NL','PL','PT','RO','SE','SI','SK'
]);
const region = EU_COUNTRIES.has(shopifyShopData.country_code)
? 'eu-west-1'
: 'us-east-1';
await db.query(
`INSERT INTO merchant_regions (shop, region, assigned_at)
VALUES ($1, $2, NOW()) ON CONFLICT (shop) DO NOTHING`,
[shop, region]
);
return region;
}
Every subsequent write, webhook, and API call for that merchant routes through their assigned region's infrastructure. No EU personal data crosses to US.
- Composite Health Check for Automated Failover
A health check that only tests HTTP 200 from the load balancer misses database failures. Test the full stack:
jsapp.get('/health/regional', async (req, res) => {
const checks = await Promise.allSettled([
primaryPool.query('SELECT 1'),
replicaPool.query('SELECT 1'),
redis.ping(),
checkQueueWorkerHealth(),
checkShopifyAPIConnectivity(),
]);
const failures = checks
.map((c, i) => ({ name: ['primary_db','replica','redis','queue','shopify'][i], ...c }))
.filter(c => c.status === 'rejected');
if (failures.length > 0) {
return res.status(503).json({
status: 'unhealthy',
region: process.env.DEPLOY_REGION,
failures: failures.map(f => ({ name: f.name, error: f.reason?.message })),
});
}
res.status(200).json({ status: 'healthy', region: process.env.DEPLOY_REGION });
});
Route 53 and Cloudflare both poll this endpoint. A 503 removes the region from routing automatically. No manual DNS intervention required.
Bonus: Hydrogen on Oxygen is Already Multi-Region
Oxygen distributes Hydrogen workers across 300+ Cloudflare edge locations automatically. The only multi-region concern is cache strategy — not routing:
js// CacheLong = resolves from Workers KV at every edge location
// Prevents distant edge nodes from making origin API calls on every request
const product = await storefront.query(PRODUCT_QUERY, {
variables: { handle: params.handle },
cache: CacheLong(), // max-age: 1hr, SWR: 23hrs
});
A 95%+ cache hit rate on product data means edge nodes almost never cross-traverse to Shopify's origin. Your storefront is globally fast by default.
The Multi-Region Decision Checklist
| Decision | Default Choice | Upgrade When |
|---|---|---|
| Topology | Active-passive | Compliance or traffic forces it |
| DNS routing | Route 53 latency-based | Already on Cloudflare |
| Database | Primary + regional replicas | Writes from multiple regions |
| Webhook dedup | Upstash Global Redis NX | Always required |
| Data residency | Shop-level region assignment | Serving EU merchants |
| Health checks | Composite (DB + Redis + queue) | Always required |
Full guide with Terraform configs, GDPR routing implementation, Cloudflare geo-steering setup, and 5-step regional failover runbook: https://kolachitech.com/multi-region-shopify-infrastructure
SOCIAL SHARE CARD GENERATOR