Cloudflare Workers無料利用枠消費削減の対処案
[Astro Frontend (Cloudflare Pages)]
↓ fetch
[Cloudflare Workers] ← 全てのpageDB読み取りリクエストがここを経由
↓
[Upstash Redis]
Cloudflare Workers無料枠 : 1日あたり100,000リクエスト
呼び出し元 :
Astroフロントエンド (astro/src/lib/pagedbAPI/getIds.ts, getPage.ts)
ユーザーのページ閲覧ごとにWorkers呼び出し
OGP一覧表示時に複数リクエスト
削減の余地 : Astro自体がCloudflare上で動作しているため、Workersを経由せずに直接Upstash Redisにアクセス可能
案
Workers削減率
実装コスト
追加コスト
備考
1. Astro API Routes統合
100%
⭐ 小
無料
最推奨
2. Cloudflare KV移行
95%+
⭐⭐ 中
無料枠内
Workersほぼ不要
3. Astro SSG + ISR
90%+
⭐⭐⭐ 大
無料
静的生成
4. バッチAPI実装
70-80%
⭐⭐ 中
無料
リクエスト数削減
5. キャッシュ統合
60-70%
⭐ 小
無料
plan.mdと併用
✅ 推奨案1: Astro API Routes統合(Workers完全排除)
Cloudflare WorkersをAstro API Routesに統合し、Workersを完全に不要にします。
Before:
[Astro Pages] → fetch → [Workers] → [Upstash Redis]
After:
[Astro Pages] → import → [Astro API Routes] → [Upstash Redis]
↓
(同一Cloudflare Pages環境内)
✅ Workers呼び出しが100%削減
✅ 追加コスト0円 (Cloudflare Pagesの無料枠: 100k requests/month + 500k requests/month for functions)
✅ レイテンシ削減 (同一環境内での処理)
✅ CORSの問題が解消
✅ 実装コストが最小 (既存コードの移行のみ)
⚠️ Workers用コードをAstro用に移行する必要
⚠️ Upstash Redis SDKの依存関係を追加
Step 1: Astro API Routesの作成
// astro/src/pages/api/pagedb/[...path].ts
import { Redis } from "@upstash/redis/cloudflare" ;
import type { APIRoute } from 'astro' ;
export const prerender = false ; // SSR mode
export const GET : APIRoute = async ( { params, request } ) => {
const runtime = Astro . locals . runtime ;
// Cloudflare環境変数から取得
const redis = Redis . fromEnv ( {
UPSTASH_REDIS_REST_URL : runtime . env . UPSTASH_REDIS_REST_URL ,
UPSTASH_REDIS_REST_TOKEN : runtime . env . UPSTASH_REDIS_REST_TOKEN ,
} ) ;
const pathArray = params . path ?. split ( '/' ) || [ ] ;
try {
const object = pathArray [ 0 ] ;
const param = decodeURIComponent ( pathArray [ 1 ] || '' ) ;
switch ( object ) {
case 'page' : {
const dataBodyEncoded = await redis . get < string > ( param ) ;
if ( dataBodyEncoded !== null ) {
const dataTyped = JSON . parse (
Buffer . from ( dataBodyEncoded , "base64" ) . toString ( )
) ;
return new Response ( JSON . stringify ( dataTyped ) , {
status : 200 ,
headers : { 'Content-Type' : 'application/json' }
} ) ;
}
return new Response (
JSON . stringify ( { error : "No item" , message : `${ param } not found` } ) ,
{ status : 404 }
) ;
}
case 'user' : {
const [ _ , keys ] = await redis . scan ( 0 , { match : `${ param } *` } ) ;
return new Response (
JSON . stringify ( { ids : keys } ) ,
{ status : 200 , headers : { 'Content-Type' : 'application/json' } }
) ;
}
default :
return new Response (
JSON . stringify ( { error : "Invalid request" } ) ,
{ status : 400 }
) ;
}
} catch ( error ) {
return new Response (
JSON . stringify ( { error : "Server error" , message : String ( error ) } ) ,
{ status : 500 }
) ;
}
} ;
# astro/.env.production
PUBLIC_GETPAGES_ENDPOINT=" /api/pagedb"
# wrangler.toml (Cloudflare Pages設定)
[env .production ]
UPSTASH_REDIS_REST_URL = " https://..."
UPSTASH_REDIS_REST_TOKEN = " ..."
Step 3: package.jsonに依存関係追加
{
"dependencies" : {
"@upstash/redis" : " ^1.28.1"
}
}
Step 4: 既存のAPI呼び出しコードは変更不要
astro/src/lib/pagedbAPI/getIds.tsとgetPage.tsは、エンドポイントURLが環境変数経由で変更されるだけで、コード変更不要。
Workers呼び出し : 100%削減(0になる)
追加コスト : 0円
レイテンシ : 30-50ms改善(Workers経由を排除)
✅ 推奨案2: Cloudflare KV移行(Workers軽量化)
Upstash RedisからCloudflare KVに移行し、Workersの呼び出しを最小化。
[Firebase Functions] → [Cloudflare KV] (書き込み)
↓
[Astro Pages] → [Astro API Routes] → [Cloudflare KV] (読み取り)
✅ Workers不要(Astro API Routesで完結)
✅ KV無料枠: 100k reads/day(十分)
✅ Upstash Redis完全排除可能
✅ 読み取りレイテンシがさらに低い
⚠️ Firebase Functionsからの書き込みロジック変更が必要
⚠️ データ移行が必要
wrangler kv:namespace create " PAGEDB"
Firebase Functionsからの書き込み
// firebase/functions/src/ogpGenerator/addPagedb.ts
export const addToCloudflareKV = async ( {
keyName,
dbData
} : {
keyName : string ,
dbData : any
} ) => {
const kvWriteUrl = `https://api.cloudflare.com/client/v4/accounts/${ ACCOUNT_ID } /storage/kv/namespaces/${ NAMESPACE_ID } /values/${ keyName } ` ;
await fetch ( kvWriteUrl , {
method : 'PUT' ,
headers : {
'Authorization' : `Bearer ${ CLOUDFLARE_API_TOKEN } ` ,
'Content-Type' : 'application/json'
} ,
body : JSON . stringify ( dbData )
} ) ;
} ;
// astro/src/pages/api/pagedb/[...path].ts
export const GET : APIRoute = async ( { params, locals } ) => {
const kv = locals . runtime . env . PAGEDB_KV ;
const pathArray = params . path ?. split ( '/' ) || [ ] ;
switch ( pathArray [ 0 ] ) {
case 'page' : {
const data = await kv . get ( pathArray [ 1 ] , { type : 'json' } ) ;
return new Response ( JSON . stringify ( data ) , { status : 200 } ) ;
}
case 'user' : {
const list = await kv . list ( { prefix : pathArray [ 1 ] } ) ;
const ids = list . keys . map ( k => k . name ) ;
return new Response ( JSON . stringify ( { ids } ) , { status : 200 } ) ;
}
}
} ;
💡 推奨案3: バッチAPI実装(リクエスト数削減)
複数のページ情報を1回のリクエストで取得できるバッチAPIを実装。
現在、ユーザーのページ一覧取得時:
GET /user/{handle} でID一覧を取得(1リクエスト)
各ページの詳細は個別に取得する実装も想定される(Nリクエスト)
→ 合計 N+1 リクエスト問題
// 新規エンドポイント: GET /user/{handle}/pages?full=true
export const GET : APIRoute = async ( { params, url } ) => {
const handle = params . handle ;
const full = url . searchParams . get ( 'full' ) === 'true' ;
const redis = Redis . fromEnv ( ...) ;
const [ _ , keys ] = await redis . scan ( 0 , { match : `${ handle } *` } ) ;
if ( ! full ) {
// ID一覧のみ
return new Response ( JSON . stringify ( { ids : keys } ) , { status : 200 } ) ;
}
// 全データを一括取得
const pipeline = redis . pipeline ( ) ;
keys . forEach ( key => pipeline . get ( key ) ) ;
const results = await pipeline . exec ( ) ;
const pages = keys . map ( ( key , i ) => ( {
id : key ,
data : results [ i ] ? JSON . parse ( Buffer . from ( results [ i ] , 'base64' ) . toString ( ) ) : null
} ) ) ;
return new Response ( JSON . stringify ( { pages } ) , { status : 200 } ) ;
} ;
🔄 推奨案4: キャッシュ統合(plan.mdとの併用)
plan.mdのCache API実装に加え、Astro側でもキャッシュを実装。
// astro/src/lib/pagedbAPI/getPage.ts
import { getLRU } from '@/utils/lruCache' ;
const cache = getLRU < pageFetchOutput > ( 100 ) ; // 最大100件キャッシュ
const CACHE_TTL = 60 * 60 * 24 * 1000 ; // 24時間
export const api = async ( { id } : { id : string } ) => {
// メモリキャッシュ確認
const cached = cache . get ( id ) ;
if ( cached && Date . now ( ) - cached . timestamp < CACHE_TTL ) {
return cached . data ;
}
// API呼び出し
const url = new URL ( object + "/" + encodeURIComponent ( id ) , endpoint_url ) ;
const data = await fetch ( url ) . then ( r => r . json ( ) ) ;
// キャッシュに保存
cache . set ( id , { data, timestamp : Date . now ( ) } ) ;
return data ;
} ;
Workers呼び出し : 70-80%削減(セッション内での再利用)
頻繁にアクセスされるページを静的生成(SSG)に移行。
// astro/src/pages/posts/[handle]/[rkey].astro
export async function getStaticPaths ( ) {
// ビルド時に人気ページのみ生成
const popularPages = await getPopularPages ( ) ; // 上位100件など
return popularPages . map ( page => ( {
params : { handle : page . handle , rkey : page . rkey } ,
props : { pageData : page . data }
} ) ) ;
}
export const prerender = true ;
✅ 人気ページはWorkers呼び出し不要
✅ レスポンスが最速
⚠️ ビルド時間増加
⚠️ 新規ページは動的に対応
Phase 1: Astro API Routes統合(即座に効果)
実装時間 : 2-3時間
Astro API Routesを作成(api/pagedb/[...path].ts)
Upstash Redis SDKをインストール
環境変数を設定
デプロイしてWorkers使用量を確認
期待効果 :
Workers呼び出し: 100%削減
追加コスト: 0円
Phase 2: Cache API統合(plan.mdと併用)
実装時間 : 1-2時間
Astro API RoutesにCache APIを追加
TTL戦略を実装(ページ: 24h、一覧: 10m)
期待効果 :
Upstash Redis読み取り: 80-95%削減
レイテンシ: 30-50ms改善
実装時間 : 4-6時間
Upstash Redis無料枠を超える場合のみ検討
Cloudflare KVに完全移行
Firebase Functionsの書き込みロジック変更
期待効果 :
Upstash Redis: 完全排除
月額コスト: $0-0.50
サービス
使用量
無料枠
超過分
Cloudflare Workers
150k req/day
100k req/day
$5/月
Upstash Redis
15k cmd/day
10k cmd/day
$0.20/月
合計
-
-
$5.20/月
案1実装後(Astro API Routes統合)
サービス
使用量
無料枠
超過分
Cloudflare Workers
0 req/day
100k req/day
$0/月
Cloudflare Pages Functions
150k req/day
100k req/month + 500k/month
$0/月
Upstash Redis
15k cmd/day
10k cmd/day
$0.20/月
合計
-
-
$0.20/月
削減額 : $5/月 → 96%コスト削減
サービス
使用量
無料枠
超過分
Cloudflare Workers
0 req/day
-
$0/月
Cloudflare Pages Functions
150k req/day
600k req/month
$0/月
Upstash Redis
2k cmd/day
10k cmd/day
$0/月
合計
-
-
$0/月
削減額 : $5.20/月 → 100%コスト削減
astro/src/pages/api/
├── pagedb/
│ └── [...path].ts # 動的ルート
└── pagedb-batch/
└── [handle].ts # バッチAPI(オプション)
// astro/src/pages/api/pagedb/[...path].ts
import { Redis } from "@upstash/redis/cloudflare" ;
import type { APIRoute } from 'astro' ;
export const prerender = false ;
// Cache APIラッパー
async function getCachedOrFetch (
cacheKey : string ,
ttl : number ,
fetchFn : ( ) => Promise < any >
) {
const cache = await caches . open ( 'pagedb-cache' ) ;
const cached = await cache . match ( cacheKey ) ;
if ( cached ) {
return cached ;
}
const data = await fetchFn ( ) ;
const response = new Response ( JSON . stringify ( data ) , {
headers : {
'Content-Type' : 'application/json' ,
'Cache-Control' : `public, max-age=${ ttl } ` ,
}
} ) ;
await cache . put ( cacheKey , response . clone ( ) ) ;
return response ;
}
export const GET : APIRoute = async ( { params, request, locals } ) => {
const runtime = locals . runtime ;
if ( ! runtime ?. env ?. UPSTASH_REDIS_REST_URL ) {
return new Response (
JSON . stringify ( { error : "Configuration error" } ) ,
{ status : 500 }
) ;
}
const redis = Redis . fromEnv ( {
UPSTASH_REDIS_REST_URL : runtime . env . UPSTASH_REDIS_REST_URL ,
UPSTASH_REDIS_REST_TOKEN : runtime . env . UPSTASH_REDIS_REST_TOKEN ,
} ) ;
const pathArray = params . path ?. split ( '/' ) || [ ] ;
const cacheKey = new URL ( request . url ) . toString ( ) ;
try {
const object = pathArray [ 0 ] ;
const param = decodeURIComponent ( pathArray [ 1 ] || '' ) ;
switch ( object ) {
case 'page' : {
// 24時間キャッシュ
return await getCachedOrFetch (
cacheKey ,
60 * 60 * 24 ,
async ( ) => {
const dataBodyEncoded = await redis . get < string > ( param ) ;
if ( dataBodyEncoded !== null ) {
return JSON . parse (
Buffer . from ( dataBodyEncoded , "base64" ) . toString ( )
) ;
}
throw new Error ( 'Page not found' ) ;
}
) ;
}
case 'user' : {
// 10分キャッシュ
return await getCachedOrFetch (
cacheKey ,
60 * 10 ,
async ( ) => {
const [ _ , keys ] = await redis . scan ( 0 , {
match : `${ param } *`
} ) ;
return { ids : keys } ;
}
) ;
}
default :
return new Response (
JSON . stringify ( { error : "Invalid request" } ) ,
{ status : 400 }
) ;
}
} catch ( error ) {
return new Response (
JSON . stringify ( {
error : error instanceof Error ? error . message : "Unknown error"
} ) ,
{ status : error instanceof Error && error . message === 'Page not found' ? 404 : 500 }
) ;
}
} ;
// OPTIONSリクエスト対応(CORS)
export const OPTIONS : APIRoute = async ( ) => {
return new Response ( null , {
status : 204 ,
headers : {
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'GET, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type' ,
}
} ) ;
} ;
Cloudflare Pagesのダッシュボードで設定:
UPSTASH_REDIS_REST_URL = "https://xxxxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN = "AXXXxxxx..."
または wrangler.toml:
[env .production .vars ]
UPSTASH_REDIS_REST_URL = " https://xxxxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN = " AXXXxxxx..."
実装後、以下の指標を監視:
リクエスト数 : 0になることを確認
削除可能性 : Phase 1完了後、Workersプロジェクト自体を削除可能
Functions呼び出し数 : Analytics > Functionsで確認
無料枠使用率 : 600k/monthに対する使用率
日次コマンド数 : ダッシュボードで確認
キャッシュ効果 : Phase 2後の削減率を測定
ページ読み込み速度 : Chrome DevTools Networkタブ
API呼び出し時間 : 改善を確認(目標: 30-50ms削減)
✅ Astro API Routesが正常動作していることを確認
✅ 本番環境で1週間テスト運用
✅ エラーログを確認
✅ バックアップとしてWorkers設定を保持
問題が発生した場合:
環境変数 PUBLIC_GETPAGES_ENDPOINT を元のWorkers URLに戻す
Astroを再デプロイ
即座に元の構成に復元可能
問題1: runtime.env が undefined
解決策 : astro.config.mjsでCloudflareアダプターが正しく設定されているか確認
import cloudflare from "@astrojs/cloudflare" ;
export default defineConfig ( {
output : "hybrid" ,
adapter : cloudflare ( ) ,
} ) ;
問題2: Buffer is not defined
解決策 : Node.js互換性を有効化
# wrangler.toml
compatibility_flags = [ " nodejs_compat" ]
または:
// ブラウザ互換のデコード
const decoded = atob ( dataBodyEncoded ) ; // base64デコード
解決策 : OPTIONSメソッドを実装(上記コード例参照)
Phase 1(Astro API Routes統合)を最優先で実装 してください。
即座に効果 : Workers呼び出しが100%削減
コスト削減 : $5/月の削減(96%削減)
実装時間 : 2-3時間で完了
リスク最小 : ロールバックが容易
追加コスト : 完全無料
1週間運用して安定性を確認
Cloudflare Workers使用量が0になったことを確認
Workersプロジェクトを削除
Phase 2(Cache API)を実装してさらに最適化
この段階的アプローチにより、リスクを最小限に抑えつつ、確実にコストを削減 できます。