Fix Google Sheets API Quota Exceeded: Read requests per minute per user

intermediate📗 Google Sheets2026-03-24| Google Sheets API v4, Node.js / Python / mọi ngôn ngữ sử dụng googleapis client, Google Cloud Console

Error Message

Quota exceeded for quota metric 'Read requests' and limit 'Read requests per minute per user' of service 'sheets.googleapis.com'
#google-sheets#api#quota#rate-limit

Lỗi Gặp Phải

Nó luôn xuất hiện vào lúc tệ nhất — giữa chừng một batch job, khi traffic đang tăng đột biến, hoặc ngay trước buổi demo:

{
  "code": 429,
  "message": "Quota exceeded for quota metric 'Read requests' and limit 'Read requests per minute per user' of service 'sheets.googleapis.com'",
  "status": "RESOURCE_EXHAUSTED"
}

Google Sheets API áp dụng giới hạn mặc định là 300 read request mỗi phút cho mỗi user đối với OAuth token. Service account có giới hạn riêng theo từng tài khoản — cũng là 300 — nhưng toàn bộ project có một ngưỡng chung là 300 read request mỗi phút tính trên tất cả service account trong project. Vượt qua một trong hai ngưỡng đó là nhận ngay lỗi 429.

Nguyên Nhân Gốc Rễ

Hầu hết vấn đề về quota đều xuất phát từ một trong ba nguyên nhân sau:

  • Vòng lặp không có throttling — duyệt qua các hàng hoặc sheet trong một vòng lặp liên tục, mỗi lần lặp gọi một request spreadsheets.values.get riêng biệt. Mười sheet = mười request. Một trăm hàng = một trăm request.
  • Nhiều worker dùng chung một service account — khi scale ngang, số lượng API call tăng theo, nhưng quota lại tính theo từng tài khoản. Bốn worker mỗi cái gọi 100 req/phút = 400 req/phút. Vượt hạn mức trước khi kịp nhận ra.
  • Retry ngay lập tức khi lỗi — retry một lỗi 429 ngay lập tức không giải quyết được gì. Ngược lại, nó còn làm mọi thứ tệ hơn. Một lần vượt quota biến thành chuỗi lỗi dây chuyền, khiến tài khoản bị throttle suốt cả khoảng thời gian một phút.

Cách Xử Lý 1: Exponential Backoff Khi Gặp Lỗi 429

Không cần thay đổi kiến trúc — chỉ cần bọc mọi API call bằng retry logic. Cách này một mình nó đã có thể ngăn hầu hết các sự cố.

Python (google-api-python-client):

import time
import random
from googleapiclient.errors import HttpError

def sheets_read_with_backoff(service, spreadsheet_id, range_name, max_retries=5):
    for attempt in range(max_retries):
        try:
            result = service.spreadsheets().values().get(
                spreadsheetId=spreadsheet_id,
                range=range_name
            ).execute()
            return result
        except HttpError as e:
            if e.resp.status == 429:
                wait = (2 ** attempt) + random.uniform(0, 1)
                print(f"Quota hit, waiting {wait:.1f}s (attempt {attempt + 1})")
                time.sleep(wait)
            else:
                raise
    raise Exception("Max retries exceeded for Sheets API")

Lần thử 1 chờ khoảng 1 giây, lần thử 2 chờ khoảng 2 giây, lần thử 3 chờ khoảng 4 giây. Đến lần thử 4, cửa sổ quota 60 giây gần như chắc chắn đã được reset.

Node.js:

const { google } = require('googleapis');

async function sheetsReadWithBackoff(sheets, spreadsheetId, range, maxRetries = 5) {
  for (let attempt = 0; attempt  setTimeout(r, wait));
      } else {
        throw err;
      }
    }
  }
  throw new Error('Max retries exceeded');
}

Cách Xử Lý 2: Gộp Nhiều Range Vào Một Lần Gọi

Đang gọi values.get mười lần trong một vòng lặp? Hãy gộp chúng lại thành một lần gọi values.batchGet. Đây thường là thay đổi có tác động lớn nhất — và chỉ cần sửa hai dòng code.

# Thay vì làm thế này:
for range_name in ['Sheet1!A1:B10', 'Sheet1!C1:D10', 'Sheet2!A1:Z1']:
    result = service.spreadsheets().values().get(
        spreadsheetId=SPREADSHEET_ID,
        range=range_name
    ).execute()

# Hãy làm thế này:
result = service.spreadsheets().values().batchGet(
    spreadsheetId=SPREADSHEET_ID,
    ranges=['Sheet1!A1:B10', 'Sheet1!C1:D10', 'Sheet2!A1:Z1']
).execute()

for value_range in result.get('valueRanges', []):
    print(value_range.get('range'), value_range.get('values'))

Ba request được gộp thành một. Nếu bạn đang lặp qua 20 range, đó là mức giảm quota 95% mà không mất bất kỳ dữ liệu nào.

Cách Xử Lý 3: Cache Kết Quả

Đọc cùng một spreadsheet từ nhiều nơi trong ứng dụng? Chỉ fetch một lần, dùng chung ở khắp nơi. Cache trong bộ nhớ với TTL 60 giây thường là đủ.

import time

_cache = {}
CACHE_TTL = 60  # giây

def get_sheet_data_cached(service, spreadsheet_id, range_name):
    key = f"{spreadsheet_id}:{range_name}"
    now = time.time()
    if key in _cache and now - _cache[key]['ts'] < CACHE_TTL:
        return _cache[key]['data']
    data = service.spreadsheets().values().get(
        spreadsheetId=spreadsheet_id,
        range=range_name
    ).execute()
    _cache[key] = {'data': data, 'ts': now}
    return data

Chạy nhiều process hoặc server? Thay dict trong bộ nhớ bằng Redis với TTL. Cùng một pattern, nhưng dùng chung giữa các worker.

Cách Xử Lý 4: Yêu Cầu Tăng Quota

Đôi khi giới hạn đơn giản là quá thấp so với nhu cầu thực tế của bạn. Bạn có thể yêu cầu tăng giới hạn trực tiếp trong Google Cloud Console — việc gửi yêu cầu hoàn toàn miễn phí.

  • Vào APIs & Services → Quotas & System Limits
  • Lọc theo sheets.googleapis.com
  • Tìm mục Read requests per minute per user
  • Nhấn biểu tượng bút chì → Edit Quota
  • Nhập giới hạn mong muốn và gửi — Google thường phản hồi trong vòng 1–2 ngày làm việc

Lưu ý thực tế: Google duyệt yêu cầu tăng quota cho Sheets API khá thận trọng. Họ kỳ vọng bạn đã tối ưu bằng batching và caching trước. Hãy chuẩn bị số liệu cụ thể — mức sử dụng hiện tại, mức sử dụng dự kiến, và lý do tại sao chỉ dùng batching thôi là chưa đủ.

Cách Xử Lý 5: Dùng Token Bucket / Rate Limiter

Nhiều worker dùng chung một quota pool cần một rate limiter dùng chung. Nếu không có, mỗi worker sẽ hoạt động như thể nó sở hữu toàn bộ 300 req/phút — và tất cả cùng chạm trần cùng một lúc.

# Dùng thư viện `ratelimit`: pip install ratelimit
from ratelimit import limits, sleep_and_retry

CALLS_PER_MINUTE = 250  # Giới hạn 300, trừ 50 để có khoảng đệm an toàn

@sleep_and_retry
@limits(calls=CALLS_PER_MINUTE, period=60)
def read_sheet(service, spreadsheet_id, range_name):
    return service.spreadsheets().values().get(
        spreadsheetId=spreadsheet_id,
        range=range_name
    ).execute()

Với các worker phân tán, hãy chuyển token bucket sang Redis để tất cả các process dùng chung một bộ đếm duy nhất.

Kiểm Tra Xem Đã Xử Lý Xong Chưa

Đừng chỉ chạy lại rồi hy vọng. Hãy kiểm tra bằng số liệu thực tế:

  • Mở Google Cloud Console → APIs & Services → Google Sheets API → Metrics
  • Theo dõi biểu đồ Quota usage — tỷ lệ lỗi 429 phải giảm về zero trong vòng một phút sau khi triển khai bản fix
  • Quét log để tìm các thông báo retry. Thỉnh thoảng thấy "waiting 1.3s (attempt 1)" là bình thường. Nhưng nếu liên tục hết cả năm lần retry thì không ổn — nghĩa là lượng request gốc vẫn còn quá cao
  • Chạy lại toàn bộ batch job và xác nhận không còn lỗi RESOURCE_EXHAUSTED nào

Phòng Ngừa

  • Backoff là bắt buộc — lỗi quota về bản chất là tạm thời. Luôn xử lý chúng theo hướng "thử lại sau", không bao giờ coi là lỗi vĩnh viễn
  • Ưu tiên dùng batchGetbatchUpdate — nếu đang xây dựng tích hợp mới, hãy bắt đầu với batch call ngay từ đầu
  • Đặt cảnh báo quota ở ngưỡng 80% trong Google Cloud Console (Monitoring → Alerting) — nhận thông báo trước khi chạm giới hạn, không phải sau khi đã vượt
  • Loại bỏ pattern polling — liên tục đọc sheet để phát hiện thay đổi sẽ đốt quota rất nhanh. Hãy dùng push notification của Google Drive thay thế; chỉ cần thiết lập một webhook thay vì hàng trăm lần polling mỗi giờ
  • Tách service account theo môi trường — traffic từ dev và staging không bao giờ nên cạnh tranh quota với production. Ba service account riêng biệt không tốn thêm chi phí gì và tránh được những bất ngờ không mong muốn

Related Error Notes