Scheduled GitHub Actions Workflow Hitting a Secured Endpoint
A GitHub Actions cron that pings a bearer-auth endpoint every N minutes. Use it for scheduled revalidation, health checks, or webhooks.
A GitHub Actions workflow that runs on a cron schedule and hits a secured endpoint with a bearer token. I use this exact pattern to drive scheduled-post revalidation on this site, but it works for any "ping a webhook every N minutes" job: health checks, cache warming, webhook retries, anything.
Tested on GitHub Actions ubuntu-latest runners.
When to Use This
- Scheduled revalidation when your hosting plan caps cron frequency
- Periodic health checks against your own API
- Triggering a webhook on a schedule (instead of needing a real cron service)
- Any job that needs "run every N minutes" without paying for a separate scheduler
Don't use this when you need sub-5-minute precision (GitHub Actions minimum is 5 min) or sub-second jitter (it's quite jittery under load).
Code
# .github/workflows/revalidate-scheduled.yml
name: Revalidate scheduled posts
on:
schedule:
- cron: '*/15 * * * *'
workflow_dispatch: {} # allow manual trigger
concurrency:
group: revalidate-scheduled
cancel-in-progress: false
jobs:
revalidate:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Hit revalidation endpoint
env:
SITE_URL: ${{ secrets.SITE_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
run: |
set -euo pipefail
if [ -z "${SITE_URL:-}" ] || [ -z "${CRON_SECRET:-}" ]; then
echo "::error::Missing SITE_URL or CRON_SECRET secrets"
exit 1
fi
response=$(curl --silent --show-error --fail-with-body \
--max-time 30 \
-H "Authorization: Bearer ${CRON_SECRET}" \
"${SITE_URL}/api/revalidate-scheduled")
echo "Response: ${response}"The concurrency block prevents two cron runs from stacking up if a previous one is still running. --fail-with-body makes curl exit with a non-zero status (failing the job) on any 4xx/5xx response, which surfaces auth or endpoint errors as red workflow runs in the GitHub UI.
Usage
The matching server endpoint that this cron calls (Next.js route handler):
// app/api/revalidate-scheduled/route.ts
import { revalidateTag } from 'next/cache';
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
const auth = request.headers.get('authorization');
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return Response.json({ ok: false }, { status: 401 });
}
revalidateTag('blogs');
revalidateTag('snippets');
return Response.json({ ok: true, at: new Date().toISOString() });
}Set CRON_SECRET and SITE_URL as repo secrets under Settings → Secrets and variables → Actions. The same CRON_SECRET value also goes into your hosting platform's environment variables.
Pitfalls
- Cron syntax is UTC.
0 9 * * *is 9 AM UTC, not 9 AM in your local timezone. Convert before scheduling. - GitHub Actions cron is jittery. Documented up to 10-15 minutes off the scheduled time during peak load. Don't rely on it for tight SLAs.
workflow_dispatch: {}is essential. Without it, you can't manually trigger the workflow from the Actions tab to test it. Always include it.- Secrets must be referenced via
secrets.NAME, notenv.NAMEdirectly. Theenv:block at the step level is what makes them available as shell variables. - Use
--fail-with-body, not just--fail. Without--fail-with-body, curl swallows the error response body, making debugging painful.
Related Snippets & Reading
- Next.js CI Workflow — for build/test on push
- Reusable Workflow with workflow_call(coming soon) — share this pattern across repos
- GitHub Actions schedule events — official cron syntax docs
Frequently Asked Questions
Why use GitHub Actions for cron instead of Vercel Cron?
Vercel Hobby caps cron jobs at daily frequency. GitHub Actions allows down to every 5 minutes, has unlimited runs on public repos, and runs independently of your hosting platform. It's the easiest free way to schedule sub-daily jobs.
Is GitHub Actions cron exact to the minute?
No. GitHub Actions cron has a documented jitter of up to 10-15 minutes during heavy load (top of the hour is the worst). Plan your timing tolerances around that. For sub-minute precision, use a paid scheduler.