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
Top comments (0)