エラーの概要
バッチジョブの途中、トラフィックスパイク時、あるいはデモの直前など、最悪のタイミングで必ず発生します:
{
"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は、OAuthトークンに対してデフォルトでユーザーあたり1分間に300リクエストという上限を設けています。サービスアカウントはアカウントごとに同じく300件の制限がありますが、プロジェクト内のすべてのサービスアカウントを合わせたプロジェクト全体の上限は1分間に300リクエストです。どちらかを超えると429エラーが発生します。
根本原因
クォータ問題のほとんどは、以下の3つのパターンのいずれかに起因します:
- スロットリングなしのループ — 行やシートをタイトなループで繰り返し処理し、各イテレーションで個別に
spreadsheets.values.getを呼び出すケース。10シートなら10リクエスト、100行なら100リクエストになります。 - 複数ワーカーが同一サービスアカウントを共有 — 水平スケーリングによりAPIコールが増倍しますが、クォータはアカウント単位です。各ワーカーが100リクエスト/分を処理する4台のワーカーは合計400リクエスト/分となり、気づく前に上限を超えます。
- 失敗時の即時リトライ — 429エラーを即座にリトライしても効果はありません。むしろ状況が悪化します。1件のクォータ超過が連鎖的な問題を引き起こし、1分間のウィンドウ全体にわたってアカウントがスロットリングされ続けます。
修正1:429エラーに対するExponential Backoff
アーキテクチャの変更は不要です。すべてのAPIコールをリトライロジックでラップするだけで、ほとんどの障害を防ぐことができます。
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"クォータ超過、{wait:.1f}秒待機中(試行 {attempt + 1})")
time.sleep(wait)
else:
raise
raise Exception("Sheets APIの最大リトライ回数を超過しました")
1回目の試行は約1秒、2回目は約2秒、3回目は約4秒待機します。4回目までには、60秒のクォータウィンドウがほぼ確実にリセットされます。
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('最大リトライ回数を超過しました');
}
修正2:複数レンジを1回のコールにまとめる
ループ内でvalues.getを10回呼んでいますか?それらを1回のvalues.batchGetにまとめましょう。これが最も効果的な改善策であることが多く、わずか2行の修正で済みます。
# 変更前:
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()
# 変更後:
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'))
3回のリクエストが1回になります。20個のレンジをループしている場合、データの損失なしにクォータを95%削減できます。
修正3:レスポンスをキャッシュする
アプリの複数箇所から同じスプレッドシートを読み込んでいますか?一度取得してすべてで共有しましょう。60秒のインプロセスキャッシュで通常は十分です。
import time
_cache = {}
CACHE_TTL = 60 # 秒
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
複数のプロセスやサーバーで動作させる場合は、インプロセスのdictをTTL付きのRedisに置き換えてください。同じパターンでワーカー間で共有できます。
修正4:クォータの増量申請
正当なワークロードに対して制限が低すぎる場合もあります。Google Cloud Consoleから直接より高い上限を申請できます。申請自体は無料です。
- APIs & Services → Quotas & System Limits に移動します
sheets.googleapis.comでフィルタリングします- Read requests per minute per user を探します
- 鉛筆アイコンをクリック → Edit Quota
- 希望する上限を入力して送信します — Googleは通常1〜2営業日以内に回答します
注意点として、GoogleはSheets APIのクォータ増量申請を保守的に審査します。まずバッチ処理とキャッシュで最適化することを求められます。現在の使用量、予測使用量、そしてバッチ処理だけでは不十分な理由を数値で示して申請してください。
修正5:トークンバケット/レートリミッターを使用する
クォータプールを共有する複数のワーカーには、共有レートリミッターが必要です。これがないと、各ワーカーが300リクエスト/分の予算をすべて自分のものとして扱い、一斉に上限に達してしまいます。
# `ratelimit` ライブラリを使用: pip install ratelimit
from ratelimit import limits, sleep_and_retry
CALLS_PER_MINUTE = 250 # 上限300からヘッドルーム50を引いた値
@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()
分散ワーカーの場合は、すべてのプロセスが単一のカウンターを共有できるよう、トークンバケットをRedisに移してください。
修正が有効かどうかの確認
実行して祈るだけでは不十分です。数値を確認しましょう:
- Google Cloud Console → APIs & Services → Google Sheets API → Metrics を開きます
- Quota usage グラフを監視します — 修正をデプロイしてから1分以内に429エラーの発生率がゼロに下がるはずです
- ログでリトライメッセージを確認します。「1.3秒待機中(試行1)」が時々表示されるのは正常です。5回のリトライをすべて使い切っている場合は問題で、基本的なコール量がまだ多すぎることを意味します
- バッチジョブをエンドツーエンドで再実行し、
RESOURCE_EXHAUSTEDエラーがゼロであることを確認します
予防策
- Backoffは必須 — クォータエラーは設計上一時的なものです。常に「後でリトライ」として扱い、ハードエラーとして扱わないでください
- デフォルトで
batchGetとbatchUpdateを使用 — 新しいインテグレーションを構築する場合は、最初からバッチコールを使いましょう - Google Cloud ConsoleでクォータアラートをUsage 80%に設定(Monitoring → Alerting)— 上限に達してからではなく、達する前に通知を受け取れます
- ポーリングパターンを排除 — 変更を検知するためにシートを繰り返し読み込むとクォータを急速に消費します。代わりにGoogle Driveのプッシュ通知を使用してください。1時間に数百回のポーリングコールではなく、1回のWebhook設定で済みます
- 環境ごとにサービスアカウントを分離 — 開発やステージングのトラフィックが本番環境のクォータと競合しないようにしましょう。3つの別々のサービスアカウントはコスト不要で、予期しない問題を防ぎます

