Refresh token Zalo OA là thứ dễ làm hỏng nhất khi bạn chạy n8n ở queue mode: worker n8n mặc định chạy **10 job song song**, nên hai workflow cùng đi đổi một token là chuyện xảy ra thật chứ không phải lý thuyết. Bài này đưa ra một thiết kế để chỉ một nơi được phép đổi token: một workflow refresher, một bảng Postgres có khoá thuê 60 giây, một đường báo lỗi.
Bài chỉ dựa vào tài liệu n8n mà tôi đã mở và đọc được thân bài. Trang tài liệu Zalo OA là ứng dụng JavaScript, công cụ của tôi chỉ đọc được tiêu đề, nên bài không đưa endpoint, tham số hay thời hạn token của Zalo. Những thông số đó bạn đọc trực tiếp ở trang chính hãng trước khi chạy thật. Phạm vi: chỉ nói về token. Việc dựng agent trả lời khách nằm ở bài [xây agent chăm sóc khách hàng với n8n](/bai-viet/xay-agent-cham-soc-khach-hang-voi-n8n).
Vì sao hai workflow n8n cùng refresh token Zalo OA sẽ làm mất token?
Giả định thiết kế: refresh token của OA chỉ đổi được một lần, đổi xong token cũ mất hiệu lực. Đây là giả định để tính rủi ro, không phải thông số tôi đã xác minh. Nếu tài liệu Zalo của bạn nói ngược lại, thiết kế vẫn đúng nhưng bớt cấp bách.
Đừng trả tiền thừa cho SaaS AI — Nhận lộ trình công cụ & chi phí tối ưu trong 10 giây
Mô tả bài toán của doanh nghiệp bạn (CSKH Zalo OA, Coding, RAG tài liệu nội bộ, Báo cáo P&L). Hệ thống cố vấn của 3A sẽ đề xuất bộ công cụ đã qua kiểm thử thực chiến.
Chuỗi sự cố nếu giả định đúng:
1. Access token hết hạn lúc T. Workflow A và B đều nhận lỗi trong cùng một giây. 2. A gửi refresh với token R0, nhận cặp mới và vô hiệu R0. 3. B vẫn cầm R0, gửi refresh và bị từ chối. 4. Mỗi workflow giữ một bản token riêng, nên hai nơi nắm hai cặp khác nhau và chỉ một cặp còn hiệu lực.
Queue mode làm chuyện này dễ xảy ra hơn. Theo [docs n8n về queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/), main nhận timer và webhook rồi đẩy execution ID vào Redis, worker lấy job từ hàng đợi. Hai lượt thực thi có thể nằm trên hai worker khác nhau, nên biến trong bộ nhớ không bảo vệ được ai. Còn static data thì chính docs n8n cảnh báo là tính năng thử nghiệm, có thể chạy không ổn định khi workflow chạy dày, và khuyên dùng data table cho dữ liệu cần giữ lại. Tôi chọn bảng Postgres vì cần khoá dòng.
| Dữ kiện n8n | Giá trị | Nguồn |
|---|---|---|
| Worker chạy song song mặc định | 10 job | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Mức concurrency n8n khuyên cho worker | 5 trở lên | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Ai xử lý timer và webhook | Tiến trình main | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Biến bật queue mode | `EXECUTIONS_MODE=queue` trên main và mọi worker | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Khoá mã hoá credential | `N8N_ENCRYPTION_KEY` của main phải chia sẻ cho mọi worker | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Database với queue mode | SQLite không khuyên dùng, nên dùng PostgreSQL | [docs.n8n.io, Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/) |
| Static data | Thử nghiệm, có thể không ổn định khi chạy dày | [docs.n8n.io, getWorkflowStaticData](https://docs.n8n.io/code/cookbook/builtin/get-workflow-static-data/) |
Cảnh báo kỹ thuật
Mất token là mất kênh Nếu refresh token bị dùng sai, nhiều khả năng phải cấp quyền lại cho ứng dụng trên OA bằng tay. Với kênh chăm sóc khách chạy 24/7, đó là sự cố nghiêm trọng. Hãy đọc trang tài liệu Zalo về cấp lại token trước khi dựng bất cứ thứ gì.
Kiến trúc refresh token Zalo OA trong n8n production gồm những gì?
Nguyên tắc: một nơi duy nhất được phép đổi token, mọi nơi khác chỉ đọc. Các thành phần (do tôi thiết kế, không phải thông số của Zalo hay n8n):
- Workflow `zalo-token-refresher`: chủ duy nhất của việc gọi endpoint refresh. Lịch chạy mỗi 6 giờ là con số tôi chọn, hãy đặt ngắn hơn hạn access token mà tài liệu Zalo ghi.
- Bảng `zalo_oa_token` trong Postgres: nguồn sự thật của cặp token và khoá thuê, một dòng cho mỗi OA.
- Bảng `zalo_token_events`: nhật ký và hàng đợi thư chết (dead-letter). Thất bại ghi kèm phản hồi thô đã che token.
- Sub-workflow `get-zalo-token`: mọi workflow khác gọi để lấy access token, chỉ đọc bảng, không gọi Zalo.
- Workflow `zalo-token-alert`: báo người khi có dòng dead-letter chưa xử lý, qua kênh không phụ thuộc Zalo.
Luồng của refresher:
1. Giành khoá thuê bằng một câu `UPDATE ... RETURNING` duy nhất. Không giành được thì thoát êm, không coi là lỗi. 2. Đọc refresh token hiện tại, gọi endpoint refresh theo tài liệu Zalo. 3. Thành công: một câu `UPDATE` ghi access token, refresh token mới, hạn dùng và nhả khoá cùng lúc. 4. Thất bại: ghi phản hồi thô vào `zalo_token_events` với `status = 'dead'`, nhả khoá, không retry tự động. Nếu lỗi là mạng mà Zalo đã đổi token ở phía họ, retry với token cũ chỉ tạo thêm lỗi. Người trực xem phản hồi thô rồi mới quyết.
Bảng Postgres giữ refresh token Zalo OA trong n8n queue mode cần những cột nào?
Phần dựng cụm n8n queue mode (Postgres, Redis, main, worker và task runner) đã có đủ trong bài [Self host n8n queue mode: cấu hình PostgreSQL, Redis và worker](/bai-viet/self-host-n8n-queue-mode-postgresql-redis-worker). Với bài toán refresh token, chỉ cần nhớ hai biến: `EXECUTIONS_MODE=queue` để job chia ra worker, và `N8N_ENCRYPTION_KEY` phải giống hệt nhau ở main và mọi worker, nếu không worker không giải mã được credential Zalo OA.
Schema cho hai bảng. Cột `lock_until` là khoá thuê: nếu refresher chết giữa chừng, khoá tự hết hạn sau 60 giây (con số tôi chọn) thay vì kẹt vĩnh viễn.
CREATE TABLE zalo_oa_token (
oa_id text PRIMARY KEY,
access_token text NOT NULL,
refresh_token text NOT NULL,
access_expires_at timestamptz NOT NULL,
lock_owner text,
lock_until timestamptz,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE zalo_token_events (
id bigserial PRIMARY KEY,
oa_id text NOT NULL,
status text NOT NULL CHECK (status IN ('ok','dead')),
expires_in integer, -- giá trị thô từ phản hồi, để đối chiếu với tài liệu Zalo
raw_response jsonb, -- chỉ ghi khi status = 'dead', và che token trước khi ghi
created_at timestamptz NOT NULL DEFAULT now(),
handled_at timestamptz -- NULL = dead-letter chưa xử lý
);
-- Bước 1 của refresher: giành khoá thuê, nguyên tử
UPDATE zalo_oa_token
SET lock_owner = $1, lock_until = now() + interval '60 seconds'
WHERE oa_id = $2 AND (lock_until IS NULL OR lock_until < now())
RETURNING refresh_token;Code node refresh token Zalo OA trong n8n chạy ra sao?
Sau khi giành được khoá, một node HTTP Request gọi endpoint refresh (lấy URL, header và body từ tài liệu Zalo, không chép từ bài này), rồi một Code node chuẩn hoá kết quả trước khi ghi. Code node không gọi mạng, chỉ xử lý dữ liệu:
// Code node (JavaScript): chuẩn hoá phản hồi refresh trước khi ghi Postgres
const res = $input.first().json;
const oaId = $('Lock').first().json.oa_id;
if (!res.access_token || !res.refresh_token) {
// dead-letter: giữ phản hồi (đã che token) cho người trực, KHÔNG retry tự động
return [{ json: { status: 'dead', oa_id: oaId, raw_response: res } }];
}
// Đơn vị của expires_in (giây hay mili giây) lấy theo tài liệu Zalo; sửa hệ số 1000 nếu khác
const expiresIn = Number(res.expires_in);
return [{
json: {
status: 'ok',
oa_id: oaId,
access_token: res.access_token,
refresh_token: res.refresh_token,
expires_in: expiresIn,
access_expires_at: new Date(Date.now() + expiresIn * 1000).toISOString(),
},
}];Node Postgres kế tiếp chạy một câu `UPDATE` ghi cặp token mới và đặt `lock_owner = NULL, lock_until = NULL` cùng lúc. Với nhánh `dead`, một node Postgres khác chèn dòng vào `zalo_token_events`, và `zalo-token-alert` bắt các dòng có `handled_at IS NULL`.
Workflow tiêu thụ chỉ làm một việc: gọi sub-workflow `get-zalo-token` để nhận access token còn hạn. Nếu `access_expires_at` còn dưới 10 phút, sub-workflow trả lỗi rõ ràng chứ không tự refresh. Gom quyền refresh về một chỗ quan trọng hơn việc tiết kiệm một lần gọi.
Đánh đổi, chi phí ẩn và giới hạn của thiết kế refresh token này là gì?
- **Một refresher duy nhất:** hết race condition giữa các workflow, đổi lại là điểm lỗi đơn. Refresher hỏng thì mọi nơi chờ token hết hạn.
- **Khoá thuê bằng Postgres:** nguyên tử và tự hết hạn khi tiến trình chết, đổi lại thêm một bảng và một câu SQL phải bảo trì.
- **Không retry tự động:** không đốt thêm lượt vào token đã đổi, đổi lại cần người trực và có độ trễ phản ứng.
- **Redis làm lớp khoá phụ:** Redis đã có sẵn vì queue mode cần. Với vài workflow, thêm khoá thứ hai là thêm nguồn sự thật thứ hai, không đáng, nên bỏ.
Cảnh báo kỹ thuật
Rủi ro, chi phí ẩn và giới hạn áp dụng - **Cửa sổ mất token còn lại:** nếu Zalo đã đổi token nhưng ghi DB thất bại (mất kết nối ngay sau phản hồi), refresh token mới chỉ còn trong dữ liệu thực thi của n8n. Cấp quyền lại là đường lùi duy nhất. Không thiết kế nào xoá được cửa sổ này. - **Bí mật trong log:** dữ liệu thực thi của n8n có thể chứa token. Hạn chế lưu dữ liệu thực thi cho các workflow này và mã hoá đĩa Postgres. Token OA mở được dữ liệu khách nên phải kiểm soát ai đọc được nó. - **Chi phí vận hành:** Postgres, Redis và các worker là thêm máy và thêm người canh. Bài này không có số chi phí; hãy lấy hoá đơn VPS và giờ công trực của chính bạn. - **Giới hạn áp dụng:** chỉ có một OA, một workflow và lưu lượng thấp thì một workflow refresh đơn giản kèm cảnh báo là đủ. Dựng cả khối này là phức tạp thừa. - **Việc bạn phải làm trước khi chạy:** đọc tài liệu Zalo OA để lấy endpoint, tham số, thời hạn access token, thời hạn refresh token và quy tắc dùng một lần, rồi thử trên OA thử, không thử trên OA đang phục vụ khách.
Nguồn tham khảo
Ngày đọc: 19/10/2026. Link n8n mở lại ngày 19/10/2026 lúc 11h15 GMT+7, đều trả HTTP 200.
- n8n Docs, Queue mode: [docs.n8n.io/hosting/scaling/queue-mode](https://docs.n8n.io/hosting/scaling/queue-mode/) (đọc 19/10/2026). Nguồn của toàn bộ bảng n8n, trừ dòng static data.
- n8n Docs, getWorkflowStaticData: [docs.n8n.io/code/cookbook/builtin/get-workflow-static-data](https://docs.n8n.io/code/cookbook/builtin/get-workflow-static-data/) (đọc 19/10/2026).
- Zalo for Developers, bài "Lấy OA Access Token V4": [developers.zalo.me/community/detail/283f7fdb439eaac0f38f](https://developers.zalo.me/community/detail/283f7fdb439eaac0f38f) (mở 19/10/2026, công cụ chỉ đọc được tiêu đề, không dùng làm căn cứ cho thông số nào trong bài; bạn tự mở để đối chiếu).
- Zalo for Developers, bài "[OA][Auth] Invalid Refresh token": [developers.zalo.me/community/detail/4d2b8ed7b2925bcc0283](https://developers.zalo.me/community/detail/4d2b8ed7b2925bcc0283) (mở 19/10/2026, cùng lưu ý trên).
— Khánh Linh, viết cho người làm thật.


