INTERMEDIATEGITHUB-ACTIONSSCHEDULING

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.

Published Apr 24, 2026
github-actionscronschedulecurlautomation

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, not env.NAME directly. The env: 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.

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.

X (Twitter)LinkedIn