A/B Migration Implementation: Parallel Operation of Old and New Site
A/B migration is an approach where old and new sites work simultaneously. Traffic is gradually switched from one to the other. This allows you to detect issues on a small audience before full switchover.
A/B Migration Architecture
DNS/CDN → Load Balancer (nginx/Cloudflare)
↓
┌────────────┴────────────┐
│ │
Old Site (90%) New Site (10%)
old-site:8080 new-site:8081
Critically important: both systems must work with a single database (or synchronize data in real-time).
Option 1: Nginx Weighted Upstream
upstream site_upstream {
server old-site:8080 weight=9;
server new-site:8081 weight=1; # 10% of traffic
}
server {
listen 80;
server_name company.com;
proxy_pass http://site_upstream;
}
Gradual switchover: 10% → 25% → 50% → 90% → 100% with intervals of several days.
Option 2: Cookie-based Routing (Stable Experience)
A user who lands on the new site stays on it for subsequent visits:
split_clients "${remote_addr}${http_user_agent}" $new_site_user {
10% "yes"; # 10% of users → new site
* "";
}
server {
set $upstream_server "old-site:8080";
# If already marked — route to new
if ($cookie_site_version = "new") {
set $upstream_server "new-site:8081";
}
# If fell into 10% — route to new and set cookie
if ($new_site_user = "yes") {
set $upstream_server "new-site:8081";
add_header Set-Cookie "site_version=new; Path=/; Max-Age=86400; SameSite=Lax";
}
proxy_pass http://$upstream_server;
}
Option 3: Cloudflare Workers for Flexible Routing
// cloudflare-worker.js
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
const cookie = request.headers.get('Cookie') || ''
// Already has version
if (cookie.includes('site_version=new')) {
return fetch(NEW_SITE_URL + url.pathname + url.search, request)
}
if (cookie.includes('site_version=old')) {
return fetch(OLD_SITE_URL + url.pathname + url.search, request)
}
// Distribute new users (hash by IP)
const clientIP = request.headers.get('CF-Connecting-IP') || ''
const hash = await hashString(clientIP)
const percentage = 10 // 10% to new site
const useNew = (hash % 100) < percentage
const targetUrl = useNew ? NEW_SITE_URL : OLD_SITE_URL
const response = await fetch(targetUrl + url.pathname + url.search, {
...request,
headers: request.headers
})
const newHeaders = new Headers(response.headers)
const cookieValue = useNew ? 'new' : 'old'
newHeaders.append('Set-Cookie', `site_version=${cookieValue}; Path=/; Max-Age=86400; SameSite=Lax`)
return new Response(response.body, {
status: response.status,
headers: newHeaders
})
}
Data Synchronization Between Sites
If old and new sites have different databases, data must be synchronized:
# Webhook on old site when content is created/updated
def on_content_updated(post_id, event_type):
payload = serialize_post(post_id)
# Send to new site
requests.post(
'http://new-site-internal/api/sync/content',
json={
'event': event_type, # 'created', 'updated', 'deleted'
'data': payload,
'timestamp': time.time()
},
headers={'X-Sync-Key': SYNC_SECRET}
)
Or via shared queue:
# RabbitMQ/Redis Streams as message bus between sites
publisher.publish('content.updated', {
'legacy_id': post_id,
'action': 'update',
'data': post_data
})
Monitoring During A/B Migration
Compare metrics of both versions:
# Grafana dashboard: Old vs New side by side
panels:
- title: "Response Time (p95)"
queries:
- {expr: "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{app='old-site'}[5m]))", legend: "Old Site"}
- {expr: "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{app='new-site'}[5m]))", legend: "New Site"}
- title: "Error Rate"
queries:
- {expr: "rate(http_requests_total{app='old-site',status=~'5..'}[5m])", legend: "Old Errors"}
- {expr: "rate(http_requests_total{app='new-site',status=~'5..'}[5m])", legend: "New Errors"}
Alert on new site anomaly:
- alert: NewSiteErrorSpike
expr: |
rate(http_requests_total{app="new-site",status=~"5.."}[2m]) >
2 * rate(http_requests_total{app="old-site",status=~"5.."}[2m])
for: 3m
annotations:
summary: "New site error rate 2x higher than old site"
Full Switchover Criteria
Before moving 100% traffic to the new site:
- Error rate ≤ error rate of old site
- p95 response time ≤ p95 of old site
- No regression in conversion (by GA4/Metrika)
- All critical functions tested on real users
- Team ready for quick rollback
Rollback
# Instant rollback via nginx
# Change weights: old-site weight=10, new-site weight=0
nginx -s reload
# Or via Cloudflare: change Worker environment variable NEW_SITE_PERCENTAGE=0
Timeline
Setting up A/B migration with gradual traffic switchover, data synchronization and monitoring — 4–7 working days.







