Version Skew trong Next.js: Lỗi ngớ ngẩn mà ai cũng gặp
User click nút mà app lăn ra lỗi sau mỗi lần deploy? Đó là version skew. Đây là cách xử lý triệt để trên mọi nền tảng.
Nguyen Nhat Long
@longnn

Version Skew trong Next.js: Lỗi ngớ ngẩn mà ai cũng gặp
Bạn vừa deploy xong, CI/CD xanh lè, Slack báo thành công. Bạn thở phào.
5 phút sau, support gửi screenshot: user click nút "Thanh toán" thì app trắng xóa, console đỏ lòm. Bạn test lại — mọi thứ chạy ngon. Refresh trang của user — hết lỗi.
Chào mừng bạn đến với version skew — cái bug mà bạn không viết ra nhưng vẫn phải chịu trách nhiệm.
Version skew là gì và tại sao Next.js lại dính nặng?
Version skew xảy ra khi browser của user vẫn chạy JavaScript bundle cũ, trong khi server đã chuyển sang version mới. Nghe đơn giản, nhưng hậu quả thì không đơn giản chút nào.
Tưởng tượng thế này: bạn deploy lúc 2 giờ chiều. Anh Minh ở phòng kế toán mở app từ sáng, tab vẫn mở. Anh ấy click vào Server Action → server nhận payload được encrypt bằng key cũ → boom, lỗi không ai hiểu nổi.

Cụ thể trong Next.js, version skew gây ra:
- Server Actions fail với lỗi encryption — vì key thay đổi mỗi build
- Dynamic imports lỗi — chunk hash thay đổi, file cũ không còn trên server
- Hydration mismatch — server render HTML mới, client cố hydrate bằng code cũ
- API response sai format — client expect shape cũ, server trả shape mới
Theo kinh nghiệm của mình, Server Actions là thứ dính nặng nhất. Với API call thông thường, bạn còn bắt được lỗi rõ ràng. Còn Server Actions fail thì error message cực kỳ khó hiểu — mình từng mất 2 tiếng debug trước khi nhận ra đó chỉ là version skew.
BUILD_ID và deploymentId — cơ chế phía sau
Mỗi lần build, Next.js tạo một BUILD_ID duy nhất trong .next/BUILD_ID. Từ Next.js 14.1.4, bạn có thể set deploymentId trong config:
1// next.config.ts2const config = {3 experimental: {4 deploymentId: process.env.VERCEL_DEPLOYMENT_ID5 || process.env.CF_PAGES_COMMIT_SHA6 || 'local-dev'7 }8}
ID này được gửi kèm request, giúp server phát hiện client đang chạy version nào. Đây là nền tảng cho mọi giải pháp skew protection.
Mẹo với Server Actions: Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY thành một giá trị cố định giữa các deployment. Điều này giữ encryption key ổn định qua các lần deploy. Nhưng lưu ý — bạn đang đánh đổi một phần security để lấy stability. Chỉ làm khi bạn hiểu rõ trade-off.
Giải pháp từ các hosting platform
Tin vui: nếu bạn đang dùng một platform lớn, khả năng cao họ đã xử lý giúp bạn rồi.

Vercel Skew Protection
Có sẵn trên Pro và Enterprise. Cơ chế: Vercel tag mỗi response với deployment ID, client gửi ID này trong request header, Vercel route request về đúng deployment tương ứng. Không cần viết code gì.
1vercel env add VERCEL_SKEW_PROTECTION_ENABLED 1
Điều mình thấy hay là Vercel giữ deployment cũ "warm" trong một khoảng thời gian configurable, nên user không bị lỗi mà vẫn dùng được app — chỉ là dùng version cũ thôi.
AWS Amplify
Tự động set AWS_AMPLIFY_DEPLOYMENT_ID và route request tương tự Vercel. Enable trong Amplify console, mục App settings > Build settings.
Netlify
Thêm một dòng config:
1NETLIFY_NEXT_SKEW_PROTECTION = true
Netlify dùng COMMIT_REF để identify version.
Cloudflare Pages (qua OpenNext)
Không có native support, nhưng OpenNext adapter hỗ trợ:
1// open-next.config.ts2export default {3 dangerous: {4 enableSkewProtection: true5 }6}
Khi platform solution chưa đủ
Các giải pháp platform có một hạn chế chung: chúng im lặng route user về code cũ. Không lỗi, nhưng user cũng không biết mình đang dùng version cũ.
Với nhiều app thì OK. Nhưng nếu bạn cần user chạy version mới nhất — security patch, hotfix, feature quan trọng — thì bạn cần chủ động thông báo cho họ reload.
Tự build version checking cho self-hosting
Nếu bạn đang host trên Railway, Render, Fly.io, hoặc Docker, không có skew protection sẵn. Mình thường làm theo 3 bước:

Bước 1 — Version endpoint:
1// app/api/version/route.ts2export const dynamic = 'force-static';34export function GET() {5 return Response.json({6 buildId: process.env.BUILD_ID || Date.now().toString()7 });8}
Bước 2 — Client hook polling:
1// hooks/use-version-check.ts2import { useEffect, useRef, useState } from 'react';34export function useVersionCheck(intervalMs = 60_000) {5 const [needsUpdate, setNeedsUpdate] = useState(false);6 const initialVersion = useRef<string | null>(null);78 useEffect(() => {9 const check = async () => {10 const res = await fetch('/api/version');11 const { buildId } = await res.json();1213 if (!initialVersion.current) {14 initialVersion.current = buildId;15 return;16 }1718 if (buildId !== initialVersion.current) {19 setNeedsUpdate(true);20 }21 };2223 check();24 const id = setInterval(check, intervalMs);25 return () => clearInterval(id);26 }, [intervalMs]);2728 return needsUpdate;29}
Bước 3 — Toast component:
1export function UpdateBanner() {2 const needsUpdate = useVersionCheck();3 if (!needsUpdate) return null;45 return (6 <div className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-lg shadow-lg">7 <p>Có phiên bản mới. Vui lòng tải lại trang.</p>8 <button onClick={() => window.location.reload()}>9 Reload ngay10 </button>11 </div>12 );13}
Đơn giản, hiệu quả, không phụ thuộc platform nào.

Những điều cần nhớ
- Version skew không phải bug của bạn, nhưng là trách nhiệm của bạn. User không quan tâm tại sao app lỗi.
- Server Actions dính nặng nhất vì encryption key thay đổi mỗi build. Cân nhắc set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYcố định nếu chấp nhận trade-off. - Platform solution chỉ ngăn lỗi, không đẩy user lên version mới. Nếu cần user chạy code mới nhất, bạn vẫn phải tự build version checking.
- Polling 60 giây là đủ cho hầu hết app. Đừng poll quá nhanh — tốn resource mà không thêm giá trị.
- Kết hợp cả hai: dùng platform skew protection để ngăn lỗi + custom version check để prompt user reload. Đây là setup mình đang dùng cho production app.
Version skew là một trong những thứ mà bạn không nghĩ tới cho đến khi nó cắn bạn lúc 11 giờ đêm. Hy vọng bài này giúp bạn xử lý trước khi nó xảy ra. Deploy vui nhé! 🚀
Nguyen Nhat Long
@longnnThấy hay? Chia sẻ cho bạn bè!
Bài viết liên quan
Có thể bạn cũng thích

Zero-Downtime Deployment: Deploy mà user không biết
Làm sao để deploy bản mới lên production mà không ai nhận ra? Cùng tìm hiểu các chiến lược Blue-Green, Rolling, Canary và cách áp dụng thực tế.
Tolaria Quản lý knowledge base bằng Markdown như dân chuyên nghiệp
Tolaria là desktop app open source giúp quản lý knowledge base bằng markdown files, git-first, offline-first. Đây có thể là thứ bạn đang thiếu cho second brain.
Floci: Giã từ LocalStack, chào đón AWS emulator miễn phí thực sự
LocalStack Community đã sunset. Floci là alternative mới — mã nguồn mở, không cần auth token, startup 24ms. Đây là lý do bạn nên thử ngay.