Skip to content

🔒 Fix In-Memory Rate Limiting in Serverless Environment#303

Open
is0692vs wants to merge 2 commits into
mainfrom
fix-serverless-rate-limiting-457086307398069304
Open

🔒 Fix In-Memory Rate Limiting in Serverless Environment#303
is0692vs wants to merge 2 commits into
mainfrom
fix-serverless-rate-limiting-457086307398069304

Conversation

@is0692vs
Copy link
Copy Markdown
Contributor

@is0692vs is0692vs commented May 22, 2026

🎯 What: Replaced the purely in-memory rate limiter with a distributed Upstash Redis-based rate limiter using @upstash/ratelimit and @upstash/redis.
⚠️ Risk: Purely in-memory rate limiting is ineffective in a serverless environment (like Vercel Edge functions) because state isn't shared across instances, allowing attackers to easily bypass limits by hitting different instances.
🛡️ Solution: Integrated Upstash Redis for shared rate limit state across serverless instances, with a graceful fallback to in-memory caching if environment variables are missing (for local dev).


PR created automatically by Jules for task 457086307398069304 started by @is0692vs

Greptile Summary

インメモリのレート制限をUpstash Redisベースの分散レート制限に置き換えることで、Vercel Edgeのようなサーバーレス環境での複数インスタンス間での状態共有を実現しています。環境変数が未設定の場合にはインメモリへのフォールバックを行うことでローカル開発との互換性も保たれています。

  • RateLimiter クラスを拡張し、Upstash Redisが設定されている場合は @upstash/ratelimitslidingWindow アルゴリズムで分散カウントを行い、そうでない場合は既存のインメモリキャッシュを使用するフォールバックを実装。
  • /api/card/:username および /api/og/:username のルートハンドラーで check() 呼び出しに await を追加し、テストも非同期対応に更新。

Confidence Score: 3/5

Redisが一時的に利用できない場合にAPIが500エラーとなる未処理の例外パスがあるため、マージ前に修正が推奨されます。

Redisクライアントのエラーハンドリングが check() メソッドに存在しないため、Upstash接続の問題がそのままAPIレスポンスの失敗に直結します。インメモリのフォールバックは実装済みですが、Redis例外をキャッチしてそのパスに切り替えるコードがないため、本来のフォールバックが機能しません。

src/lib/rateLimit.ts の check() メソッドにおけるエラーハンドリングを重点的に確認してください。

Important Files Changed

Filename Overview
src/lib/rateLimit.ts Upstash Redisによる分散レート制限を実装。ただしRedisエラー時の例外ハンドリングが欠如しており、Redis障害時にAPIが500エラーとなるリスクがある。
src/lib/tests/rateLimit.test.ts テストをasync対応に更新。インメモリフォールバックのテストカバレッジは維持されているが、Upstash統合パスのテストは追加されていない。
src/app/api/card/[username]/route.ts rateLimiter.check() の呼び出しを await 付きに更新。それ以外の変更なし。
src/app/api/og/[username]/route.tsx rateLimiter.check() の呼び出しを await 付きに更新。それ以外の変更なし。
package.json @upstash/ratelimit@upstash/redis の依存関係を追加。

Sequence Diagram

sequenceDiagram
    participant Client
    participant EdgeFunction as Vercel Edge Function
    participant RateLimiter as RateLimiter
    participant Upstash as Upstash Redis
    participant InMemory as In-Memory Cache

    Client->>EdgeFunction: GET /api/card/:username (or /api/og/:username)
    EdgeFunction->>RateLimiter: await check(ip)

    alt Upstash 環境変数あり
        RateLimiter->>Upstash: upstashRatelimit.limit(key)
        Upstash-->>RateLimiter: "{ success, reset }"
        Note over RateLimiter,Upstash: エラー時は現状500エラーが伝播
    else Upstash 環境変数なし (ローカル開発)
        RateLimiter->>InMemory: キャッシュ参照・更新
        InMemory-->>RateLimiter: "{ success, reset }"
    end

    RateLimiter-->>EdgeFunction: "{ success, reset }"

    alt "success = false"
        EdgeFunction-->>Client: 429 Too Many Requests
    else "success = true"
        EdgeFunction->>EdgeFunction: ビジネスロジック実行
        EdgeFunction-->>Client: 200 OK
    end
Loading

Comments Outside Diff (1)

  1. src/lib/__tests__/rateLimit.test.ts, line 1-41 (link)

    P2 Upstash統合テストが存在しない

    テストスイートはインメモリのフォールバック動作のみを検証しており、UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN が設定された場合のUpstash経由のパスが全くテストされていません。Ratelimit をモックして、Upstashが success: false を返す場合や例外をスローする場合の挙動を確認するテストケースを追加することを検討してください。

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/__tests__/rateLimit.test.ts
    Line: 1-41
    
    Comment:
    **Upstash統合テストが存在しない**
    
    テストスイートはインメモリのフォールバック動作のみを検証しており、`UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` が設定された場合のUpstash経由のパスが全くテストされていません。`Ratelimit` をモックして、Upstashが `success: false` を返す場合や例外をスローする場合の挙動を確認するテストケースを追加することを検討してください。
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/lib/rateLimit.ts:29-33
Redisエラー時のフォールバック処理が不足しています。`upstashRatelimit.limit(key)` がネットワーク障害や接続エラーでthrowした場合、例外がそのまま呼び出し元まで伝播し、APIが500エラーを返します。Redisが一時的に利用できない状況では、レート制限が機能しなくなる代わりにAPIそのものがダウンするため、インメモリのフォールバックに切り替える方が安全です。

```suggestion
    async check(key: string): Promise<{ success: boolean; reset: number }> {
        if (this.upstashRatelimit) {
            try {
                const { success, reset } = await this.upstashRatelimit.limit(key);
                return { success, reset };
            } catch {
                // Redis が一時的に利用不可の場合はインメモリフォールバックで継続
            }
        }
```

### Issue 2 of 2
src/lib/__tests__/rateLimit.test.ts:1-41
**Upstash統合テストが存在しない**

テストスイートはインメモリのフォールバック動作のみを検証しており、`UPSTASH_REDIS_REST_URL` / `UPSTASH_REDIS_REST_TOKEN` が設定された場合のUpstash経由のパスが全くテストされていません。`Ratelimit` をモックして、Upstashが `success: false` を返す場合や例外をスローする場合の挙動を確認するテストケースを追加することを検討してください。

Reviews (1): Last reviewed commit: "🔒 Fix In-Memory Rate Limiting in Server..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Co-authored-by: is0692vs <135803462+is0692vs@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
github-user-summary Ignored Ignored May 22, 2026 8:00am

@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

リリースノート

  • 新機能

    • レート制限に Upstash Redis を統合し、スケーラブルなクラウドベースのレート制限を実装しました。
  • バグ修正

    • API エンドポイントのレート制限チェックを正しく非同期で待機するように修正しました。
  • テスト

    • レート制限機能のテストカバレッジを拡張し、クラウド統合時とフォールバック時の両方をカバーしました。

Walkthrough

このプルリクエストは、RateLimiter を Upstash Redis ベースの非同期実装に移行し、フォールバック機能を保持しながら API ハンドラを対応させ、包括的なテスト・設定を追加しています。

Changes

Upstash レート制限統合

Layer / File(s) Summary
依存関係と RateLimiter 実装
package.json, src/lib/rateLimit.ts
@upstash/ratelimit@upstash/redis を依存関係に追加。RateLimiter コンストラクタで環境変数に応じて Upstash Redis / Ratelimit インスタンスを初期化。check メソッドを async に変更し、Upstash 利用時は limit() を、それ以外はインメモリキャッシュロジックを実行、いずれも { success, reset } を返却する Promise を返す。
API ハンドラ更新
src/app/api/card/[username]/route.ts, src/app/api/og/[username]/route.tsx
GET ハンドラで rateLimiter.check(ip) 呼び出しに await を追加し、非同期レート制限判定の結果を待機。
テスト: モック・フェールバック・Upstash 検証
src/lib/__tests__/rateLimit.test.ts
@upstash/ratelimit@upstash/redis をモック化。beforeEach/afterEach で process.env とタイマー状態をリセット。インメモリフェールバック (制限未満許可、超過ブロック、ウィンドウ経過後リセット、遅延クリーンアップ) と Upstash Redis 統合 (環境変数設定時に limit() 結果を伝播) を async/await でテスト。
テストカバレッジ設定
vitest.config.ts
coverage.include に src/lib/rateLimit.ts を追加してカバレッジ対象に含める。

Sequence Diagram

sequenceDiagram
  participant Handler as API Handler
  participant RateLimiter
  participant Upstash as Upstash Redis
  participant Cache as In-memory Cache
  Handler->>RateLimiter: await check(ip)
  alt Upstash 設定あり
    RateLimiter->>Upstash: limit(key)
    Upstash-->>RateLimiter: {success, reset}
  else Upstash 設定なし
    RateLimiter->>Cache: 内メモリ キャッシュ確認
    Cache-->>RateLimiter: {success, reset}
  end
  RateLimiter-->>Handler: Promise<{success, reset}>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Upstash の雲へ駆ける、レート制限

メモリも、Redis も、両方の道

非同期に待ちて、応答を得る

テスト完備で、安心の統合よ🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルはサーバーレス環境におけるインメモリレート制限の問題修正という主要な変更内容を明確に要約しており、Upstash Redis統合による分散レート制限実装の必要性を直接的に反映している。
Description check ✅ Passed 説明はプルリクエストの目的・リスク・ソリューションを具体的に記述し、変更内容全体に関連している。Upstash Redis統合とインメモリフォールバックの実装について明確に説明されている。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-serverless-rate-limiting-457086307398069304

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Upstash Redis-based rate limiting to the application, updating the RateLimiter class to support distributed limiting while maintaining an in-memory fallback. The check method has been converted to an asynchronous function, necessitating updates across API routes and test suites. Feedback highlights the need for robust error handling around the Upstash network request; specifically, it is recommended to wrap the call in a try...catch block to prevent API failures and ensure a graceful fallback to in-memory limiting if the Redis service is unavailable.

Comment thread src/lib/rateLimit.ts
Comment on lines +30 to +33
if (this.upstashRatelimit) {
const { success, reset } = await this.upstashRatelimit.limit(key);
return { success, reset };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to this.upstashRatelimit.limit(key) performs a network request to the Upstash Redis service. If the service is unreachable or encounters a timeout, this call will throw an exception, causing the entire API route to fail with a 500 error. It is recommended to wrap this call in a try...catch block and fall back to the in-memory limiter to ensure the application remains functional. When implementing this fallback, ensure the error is logged to facilitate debugging, as per repository guidelines for handling suppressed rejections.

References
  1. When suppressing unhandled promise rejections, log the error instead of silently ignoring it to facilitate debugging.

Comment thread src/lib/rateLimit.ts
Comment on lines +29 to +33
async check(key: string): Promise<{ success: boolean; reset: number }> {
if (this.upstashRatelimit) {
const { success, reset } = await this.upstashRatelimit.limit(key);
return { success, reset };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Redisエラー時のフォールバック処理が不足しています。upstashRatelimit.limit(key) がネットワーク障害や接続エラーでthrowした場合、例外がそのまま呼び出し元まで伝播し、APIが500エラーを返します。Redisが一時的に利用できない状況では、レート制限が機能しなくなる代わりにAPIそのものがダウンするため、インメモリのフォールバックに切り替える方が安全です。

Suggested change
async check(key: string): Promise<{ success: boolean; reset: number }> {
if (this.upstashRatelimit) {
const { success, reset } = await this.upstashRatelimit.limit(key);
return { success, reset };
}
async check(key: string): Promise<{ success: boolean; reset: number }> {
if (this.upstashRatelimit) {
try {
const { success, reset } = await this.upstashRatelimit.limit(key);
return { success, reset };
} catch {
// Redis が一時的に利用不可の場合はインメモリフォールバックで継続
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/rateLimit.ts
Line: 29-33

Comment:
Redisエラー時のフォールバック処理が不足しています。`upstashRatelimit.limit(key)` がネットワーク障害や接続エラーでthrowした場合、例外がそのまま呼び出し元まで伝播し、APIが500エラーを返します。Redisが一時的に利用できない状況では、レート制限が機能しなくなる代わりにAPIそのものがダウンするため、インメモリのフォールバックに切り替える方が安全です。

```suggestion
    async check(key: string): Promise<{ success: boolean; reset: number }> {
        if (this.upstashRatelimit) {
            try {
                const { success, reset } = await this.upstashRatelimit.limit(key);
                return { success, reset };
            } catch {
                // Redis が一時的に利用不可の場合はインメモリフォールバックで継続
            }
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Co-authored-by: is0692vs <135803462+is0692vs@users.noreply.github.com>
@pull-request-size pull-request-size Bot added size/L and removed size/M labels May 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/rateLimit.ts`:
- Around line 29-33: The call to this.upstashRatelimit.limit(key) in the check()
method can reject and bubble up a 500; wrap that call in a try/catch inside
check(), log the error, and on failure return the result from the in-memory
fallback rate limiter (i.e., call the class's in-memory limiter method/field
instead of letting the exception propagate). Specifically, modify check() so it
tries await this.upstashRatelimit.limit(key) but on catch returns the equivalent
response from your local/in-memory limiter (and include a brief error log),
ensuring callers like route handlers calling rateLimiter.check(ip) never get an
unhandled exception.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fa290fa4-e0db-4cc4-9815-7d95c9f542b9

📥 Commits

Reviewing files that changed from the base of the PR and between 4020bb3 and 76c1d2a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • src/app/api/card/[username]/route.ts
  • src/app/api/og/[username]/route.tsx
  • src/lib/__tests__/rateLimit.test.ts
  • src/lib/rateLimit.ts
  • vitest.config.ts

Comment thread src/lib/rateLimit.ts
Comment on lines +29 to +33
async check(key: string): Promise<{ success: boolean; reset: number }> {
if (this.upstashRatelimit) {
const { success, reset } = await this.upstashRatelimit.limit(key);
return { success, reset };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of src/lib/rateLimit.ts
if [ -f "src/lib/rateLimit.ts" ]; then
  nl -ba src/lib/rateLimit.ts | sed -n '1,220p'
else
  echo "Missing file: src/lib/rateLimit.ts"
fi

# Find where RateLimiter.check is called
rg -n "rateLimit\.check\(|\.check\(\s*key|new RateLimiter|class RateLimiter" -S src || true

# Specifically search for ".check(" usage on rate limiter instances
rg -n "\.check\(" -S src/lib src || true

# Search for "upstashRatelimit.limit" usage
rg -n "upstashRatelimit\.limit|upstashRatelimit" -S src || true

Repository: Hiroki-org/github-user-summary

Length of output: 117


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant portion of src/lib/rateLimit.ts
if [ -f "src/lib/rateLimit.ts" ]; then
  echo "=== src/lib/rateLimit.ts (1-220) ==="
  sed -n '1,220p' src/lib/rateLimit.ts | cat -n
else
  echo "Missing file: src/lib/rateLimit.ts"
fi

echo "=== search: RateLimiter.check call sites ==="
rg -n "rateLimit\.check\(|new RateLimiter|class RateLimiter" -S src || true

echo "=== search: .check( usage ==="
rg -n "\.check\(" -S src/lib src || true

echo "=== search: upstashRatelimit.limit / upstashRatelimit ==="
rg -n "upstashRatelimit\.limit|upstashRatelimit" -S src || true

Repository: Hiroki-org/github-user-summary

Length of output: 6004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== src/app/api/og/[username]/route.tsx (1-140) ==="
sed -n '1,160p' src/app/api/og/[username]/route.tsx | cat -n

echo "=== src/app/api/card/[username]/route.ts (1-140) ==="
sed -n '1,180p' src/app/api/card/[username]/route.ts | cat -n

echo "=== src/lib/__tests__/rateLimit.test.ts (1-200) ==="
sed -n '1,240p' src/lib/__tests__/rateLimit.test.ts | cat -n

Repository: Hiroki-org/github-user-summary

Length of output: 12427


Upstash 側 limit() の reject がそのまま例外伝播して 500 になり得るため、check() で try/catch して in-memory にフォールバックすべきです

src/lib/rateLimit.ts (29-33) で this.upstashRatelimit.limit(key) が reject した場合に例外を握りつぶさず、そのまま src/app/api/og/[username]/route.tsx / src/app/api/card/[username]/route.tsrateLimiter.check(ip) 呼び出しへ伝播し、両 route とも check() 周りを try/catch していないためです。

💡 提案差分
 async check(key: string): Promise<{ success: boolean; reset: number }> {
   if (this.upstashRatelimit) {
-      const { success, reset } = await this.upstashRatelimit.limit(key);
-      return { success, reset };
+      try {
+          const { success, reset } = await this.upstashRatelimit.limit(key);
+          return { success, reset };
+      } catch {
+          // Upstash到達不可時は in-memory fallback に委譲
+      }
   }

   // Fallback to in-memory caching
   const now = Date.now();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rateLimit.ts` around lines 29 - 33, The call to
this.upstashRatelimit.limit(key) in the check() method can reject and bubble up
a 500; wrap that call in a try/catch inside check(), log the error, and on
failure return the result from the in-memory fallback rate limiter (i.e., call
the class's in-memory limiter method/field instead of letting the exception
propagate). Specifically, modify check() so it tries await
this.upstashRatelimit.limit(key) but on catch returns the equivalent response
from your local/in-memory limiter (and include a brief error log), ensuring
callers like route handlers calling rateLimiter.check(ip) never get an unhandled
exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant