ZennをGitHub連携で運用していて、3冊目の本をpushしたら、本がデプロイされませんでした。原因は「投稿数の上限」でした。何が起きたのか、公式FAQの内容、そして私が入れた「上限が解けたら出し直す」仕組みを、実際のコードと一緒に書きます。
最初に断っておくと、この記事は上限を回避する方法ではありません。上限は、コンテンツの乱造を防ぐための仕組みです。ここで書くのは、上限を守ったうえで、解けた時点で出し直すための、小さな自動化です。
起きたこと
3冊目の本(books/laravel-instagram-reels)を、published: trueにしてpushしました。Zennの「GitHub連携」画面の「最近のデプロイ」には、次のような警告が出ました(要点)。
次の本は投稿数の上限に達したためデプロイされませんでした: (本のslug)。詳細はよくある質問ページをご確認ください。
同じpushに含めた、ほかのファイル(記事の更新)は、デプロイされていました。本だけが、上限で止まった形です。本のページは、404のままでした。
公式FAQに書いてあること
Zennの「投稿数の上限とは?」(https://zenn.dev/faq/rate-limit)の要点です。
- 記事・本・スクラップは、それぞれ独立した上限が設けられている
- 記事は直近24時間以内の投稿数(投稿予約中を含む)、本は直近1週間以内の投稿数で判定される
- 上限の中身は、不正防止のため非公開
- 上限に達しても、一定時間が経てば、再び投稿できる
上限は、ユーザーごとに、複数の指標で決まるそうです。私の環境で言えること以上のことは、ここでは書けません。
私の場合の経過
- 2冊目の本を公開したのは、10月5日(15:03 UTC)
- 3冊目をpushしたのは、10月9日。上限で、デプロイされませんでした
- 3冊目が実際に出たのは、10月12日の15:49 UTC。2冊目の公開から、1週間と約45分後でした
「1週間の窓」という公式の説明と、ちょうど合う結果でした。ただし、これは私のアカウントでの1回の観察です。上限の数や条件は、人によって違うはずなので、同じになるとは限りません。
「解けたら出し直す」自動化
上限が解けた日時は、正確には分かりません。そこで、毎日、「本がまだ出ていなければ、空コミットをpushして、デプロイをやり直させる」ワークフローを入れました。
name: Retry Zenn book deploy
on:
schedule:
- cron: '30 15 * * *'
workflow_dispatch: {}
permissions:
contents: write
jobs:
retry:
runs-on: ubuntu-latest
env:
BOOK_SLUG: your-book-slug
DEADLINE: '2026-10-31'
steps:
- uses: actions/checkout@v4
- name: Push an empty commit if the book is not live yet
run: |
if [ "$(date -u +%s)" -ge "$(date -u -d "$DEADLINE" +%s)" ]; then
echo "期限を過ぎているので何もしません。"
exit 0
fi
code=$(curl -s -o /dev/null -w "%{http_code}" "https://zenn.dev/<ユーザー名>/books/$BOOK_SLUG")
if [ "$code" = "200" ]; then
echo "本はZennに出ています。何もしません。"
exit 0
fi
git config user.name "zenn-book-retry-bot"
git config user.email "actions@users.noreply.github.com"
git commit --allow-empty -m "Zenn本のデプロイ再試行(投稿数の上限が解けるまで1日1回)"
git push
ポイントは3つです。
- 本のページが200を返したら、何もしない: 出たあとは、空コミットが増えません
-
期限(
DEADLINE)を決める: 何かの理由で、永遠に出なくても、空コミットを増やし続けないためです - 1日1回だけ: 上限の窓(1週間)に対して、頻繁に叩く意味はありません。連打は、むしろ避けたほうがいいと考えています
空コミットをpushすると、Zennの連携がもう一度走ります。ボットのpushでも、Zennのデプロイは動きました(記事の自動公開でも、同じ形で使っています)。
なお、何もしなかった場合に、Zenn側が自動で出し直してくれるかは、私は確かめていません。確実に、決まった時期に出したかったので、こちらで出し直す形にしました。
記事の側: 「前回から47時間」のガード
記事の公開も、GitHub Actionsで自動化しています(キューの先頭をpublished: trueにして、1日1本)。ここでは、記事の「24時間」の窓を踏まないために、スクリプトの側に、余裕を持たせています。
// Zennの「前回公開から24時間以内は不可」制限に、cronの実行タイミングに関係なく
// 確実に引っかからないよう、バッファを持たせる(当初は26時間。下の「追記」を参照)。
const MIN_INTERVAL_HOURS = 47;
// ...
if (hoursSince < MIN_INTERVAL_HOURS) {
console.log(`前回の公開から${hoursSince.toFixed(1)}時間しか経っていません(${MIN_INTERVAL_HOURS}時間必要)。今回は公開せず終了します。`);
return;
}
cronは毎日同じ時刻に動きますが、実行時刻には数分〜数十分の揺れがあります。「24時間ぴったり」で判定すると、揺れで窓に引っかかります。そこで、バッファを持たせて、毎日cronを動かしても、実際には2日に1本のペースになる形にしました(24時間後の実行は、必要な時間に足りずに見送られ、48時間後に出ます)。
追記: 26時間では足りなかった
最初は、このバッファを26時間にしていました。ところが、この記事を公開した日(10月19日)に、cronが約2時間遅れて動き、前回の公開からちょうど26時間後になって、ガードを通過しました。本来は2日おきのつもりが、約1日あとに公開されたことになります。24時間の窓は守れていましたが、想定のペースとは違いました。
そこで、バッファを47時間に変えました。24時間後の実行は見送られ、48時間後の実行で公開されます。cronが1時間以内に遅れても、同じ結果です。cronの遅れは、数分では済まないことがあるので、「24時間+少し」ではなく、目標の間隔(48時間)の少し手前で判定するのが、安全でした。
上限は、乱造を防ぐための仕組み
Zennは、AIを使った執筆を禁止していません。ただし、「人が主体」であること、つまり公開前に、著者自身が内容の正確性を検証することを求めています。検証が追いつかない速度での投稿は、アカウントの凍結の対象になり得る、とも書かれています。
上限は、そのための仕組みです。自動化をするなら、上限を避ける工夫ではなく、上限を守る自動化にするべきだと、私は考えています。
まとめ
- Zennの上限は、記事(直近24時間)・本(直近1週間)・スクラップで独立している。数は非公開
- 本が上限で止まると、デプロイされず、画面に警告が出る
- 私の場合は、前回の本の公開から、約1週間+45分で、出し直しが通った
- 「解けたら出し直す」自動化は、1日1回・出たら何もしない・期限つきにする
- 記事の公開は、24時間ぴったりではなく、**目標の間隔の少し手前(私の場合は47時間)**で判定すると、cronの遅れに強い。最初の26時間では、約2時間の遅れで、想定より早く公開された
この記事はZennにも投稿しています。