はじめに
Persisted Queries(永続化クエリ)は、クエリ本文を毎回送らず、ハッシュ(通常はSHA-256)だけを送る方式。クライアントとサーバで事前にクエリを「登録(永続化)」しておくか、初回だけ本文を送り、以後はハッシュのみで呼び出します(Automatic Persisted Queries/APQ)。
メリットは3つ:①帯域削減(モバイルで効く)、②キャッシュ性向上(ハッシュ=安定キー)、③セキュリティ(allow-list/safelistで未知クエリを拒否)。本記事は仕組み→導入→CDN設計→セーフリスト運用→トラブル対処の順に、コードと意図で解説します。
1. 仕組みの全体像
1-1. 2段階リクエスト(APQの基本)
APQは「まずハッシュだけ送る→サーバが未登録なら本文も送って登録→以後はハッシュのみ」というプロトコル。本文送信は最初だけなので、以降はクエリが長くても転送量は一定です。
# クライアント送信(概念)
# 1回目: { extensions: { persistedQuery: { version:1, sha256Hash } }, operationName, variables }
# サーバ応答: 404/200(未登録なら "PersistedQueryNotFound")
# 2回目(未登録時のみ): 1回目 + query 本文 → サーバが保存
# 3回目以降: 1回目と同じでOK(キャッシュが効く)
意図:ハッシュを安定キーにすることで、CDNやHTTPキャッシュと相性が良く、エッジで高速応答が可能。
1-2. Safelist(許可リスト)運用
本番は「登録済みクエリのみ許可」が推奨。未知のハッシュや本文を含むリクエストは拒否。
CIでクエリ抽出→ハッシュ化→マニフェスト生成→サーバ配置を自動化しましょう。
2. サーバ導入(Apollo ServerのAutomatic Persisted Queries)
2-1. 最小実装(Node/Apollo Server)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled'; // 任意
import { createPersistedQueryPlugin } from '@apollo/server-plugin-persisted-queries';
import crypto from 'crypto';
const typeDefs = `#graphql
type Query { hello: String! }
`;
const resolvers = { Query: { hello: () => "world" } };
const server = new ApolloServer({
typeDefs, resolvers,
plugins: [
ApolloServerPluginInlineTraceDisabled(),
createPersistedQueryPlugin({
hash: (s: string) =>
crypto.createHash('sha256').update(s).digest('hex')
})
],
});
startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {
console.log(`🚀 ${url}`);
});
意図:createPersistedQueryPluginがAPQを処理。ハッシュはSHA-256でOK。プロダクションは外部キャッシュ(Redis等)で横展開に耐える。
2-2. クライアント(Apollo Client)設定
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import sha256 from 'crypto-js/sha256';
const client = new ApolloClient({
link: createPersistedQueryLink({ sha256 })
.concat(new HttpLink({ uri: '/graphql' })),
cache: new InMemoryCache(),
});
意図:リンクが自動で「ハッシュのみ→未登録なら本文送信」にフォールバック。モバイル/低帯域に効く。
2-3. セーフリスト(固定化)モード
// 擬似例:unknown hash を拒否
const safelist = new Set(JSON.parse(fs.readFileSync('pq-manifest.json','utf8')));
app.post('/graphql', (req, res, next) => {
const hash = req.body?.extensions?.persistedQuery?.sha256Hash;
if (hash && safelist.has(hash)) return next();
return res.status(400).json({ errors: [{ message: 'PersistedQueryNotAllowed' }]});
});
意図:本番は固定化、検証はAPQ(自動登録)など段階で使い分け。
3. CDNキャッシュ設計とHTTP
3-1. ハッシュをキーにキャッシュ
ハッシュ(sha256Hash)は安定キー。CDNのCache Keyに含める。GET化、またはPOSTでもボディをキーにできる基盤なら高ヒット可。
# 例:NGINXでPOSTボディ(hash)をキャッシュキーに反映(概念)
map $request_body $pqkey {
"~sha256Hash\":\"([a-f0-9]{64})\"" $1;
default "";
}
proxy_cache_key "$scheme$request_method$host$request_uri$pqkey";
意図:「本文」ではなく「ハッシュ」をキーにして高圧縮・高ヒットを狙う。
3-2. レスポンスのキャッシュ制御
Cache-Controlはpublic, max-age(個人情報なし)。認可付きはprivate, no-store。モバイルはETag/If-None-Matchも有効。
3-3. 変数(variables)
変数でレスポンスが揺れる。不変IDフェッチ(詳細系)のみ積極キャッシュ、一覧/検索は短寿命が現実的。
4. モバイル/大規模運用の実践ポイント
4-1. モバイル最適化
APQで帯域削減、さらにHTTP/2多重化とgzip/br圧縮。エラー時の本文リトライは抑制、Backoffで電池消費を下げる。
4-2. スキーマ進化とロールアウト
ビルドでクエリ抽出→ハッシュ生成→pq-manifest.json出力→配信を自動化。サーバは青/緑で旧新を同時許可、ロールバック可能に。
4-3. Relay/他クライアント
Relayはビルド時に識別子生成でPersisted Query対応可。relay-compilerのプラグイン/ネットワーク層でハッシュ送信へ。
5. トラブルシューティング
5-1. PersistedQueryNotFound 多発
キャッシュ消失(再起動/スケールアウト)やハッシュ不一致。外部キャッシュ採用、ハッシュ計算(バイト順/改行)の差異を点検。
5-2. 未知の本文が届く
本番は本文付きリクエストを拒否。WAF/ルールでextensions.persistedQueryなしを遮断、Safelist限定に。
5-3. CDNヒット率が低い
キーにハッシュが入っていない、variables揺らぎ、Authorizationで共有キャッシュ無効、が典型。パブリック読み取り系を分離して最適化。
コード断片:ビルド時にクエリ→ハッシュ→マニフェスト
CIで.graphqlを集め、ハッシュ化してJSONへ。サーバのSafelistとして読み込みます。
// build-scripts/persist.js(概念)
const fs = require('fs'); const crypto = require('crypto'); const globby = require('globby');
(async () => {
const files = await globby('src/**/*.graphql');
const map = {};
for (const f of files) {
const query = fs.readFileSync(f, 'utf8').trim();
const hash = crypto.createHash('sha256').update(query).digest('hex');
map[hash] = query;
}
fs.writeFileSync('dist/pq-manifest.json', JSON.stringify(map, null, 2));
})();
意図:「hash → query」辞書を配布。APQはミス時のみ本文が必要だが、Safelist固定運用では辞書参照で本文不要にできる。
まとめ
Persisted Queriesは、GraphQLの帯域・キャッシュ・セキュリティを同時に改善。まずはAPQ導入→本番はSafelist固定→外部キャッシュとCDNでハッシュをキーに最適化、という段階導入が現実的。モバイルや大規模配信で特に効きます。

