2026-03-08
Cron senza server: GitHub Actions che chiama la tua API (scheduled webhook)
Pattern tecnico: workflow su schedule, POST autenticato verso l’app, limiti di precisione e minuti Actions. In coda: pacchetto Snowinch @snowinch/githubcron che genera i workflow.
Problema che risolvi: hai un’app su Vercel / Netlify / Cloudflare e ti serve un job periodico (report, sync, reminder) senza VPS sempre acceso e senza subito AWS EventBridge.
Cosa fai in ~20 minuti: un workflow GitHub Actions su schedule che fa POST al tuo endpoint con un secret condiviso.
Come sai che funziona: nella tab Actions vedi run verdi e nei log del server la richiesta autenticata.
Il pattern “scheduled webhook”
- GitHub esegue il workflow all’ora definita (
cronin UTC — vedi Events that trigger workflows). - Un solo step fa
curl(ofetchin Node) versohttps://tua-app.example/api/cron/.... - L’app verifica header tipo
Authorization: Bearer …confrontando conCRON_SECRETin env. - L’handler esegue la logica (DB, email, ecc.) e risponde
200.
La logica resta nell’app: Actions è solo un sveglia affidabile quanto basta per molti casi d’uso interni.
Workflow minimale (YAML)
Salva in .github/workflows/cron-daily.yml (adatta URL e nome segreto):
name: daily-cron-webhook
on:
schedule:
- cron: '0 9 * * *' # ogni giorno 09:00 UTC — verifica fuso vs business
workflow_dispatch: # permette run manuale dalla UI
jobs:
call-app:
runs-on: ubuntu-latest
steps:
- name: POST /api/cron/daily
run: |
curl -fsS -X POST "${{ secrets.APP_CRON_URL }}" \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}" \
-H "Content-Type: application/json" \
-d '{}'
APP_CRON_URL: es.https://www.example.com/api/cron/dailyCRON_SECRET: stringa lunga random; stesso valore in env produzione dell’app.
Sicurezza: non loggare il secret; ruotalo se esposto; limita l’endpoint a operazioni idempotenti dove possibile.
Limiti reali (da non sottovalutare)
- Jitter e ritardo:
schedulenon è real-time; GitHub documenta che l’esecuzione può ritardare in periodi di alto carico. Non usare questo pattern per trading, aste o pagamenti con finestra di secondi. - Minuti Actions: i piani gratuiti hanno un budget minuti/mese — conta durata job × frequenza. Un job di 5 min ogni ora può esaurire rapidamente il free tier.
- Timeout dell’hosting: il POST deve finire dentro il limite della tua funzione (es. 10s su Vercel Hobby). Se il lavoro è lungo: accoda un job interno (DB/queue) e rispondi subito
202. - Repo pubblico: il file YAML è visibile; i secret no, ma evita di mettere URL con token in chiaro nel YAML.
Alternative quando il pattern non basta
- Queue + worker (BullMQ, Cloud Tasks, SQS): per carichi lunghi o retry fine.
- Cron del provider (Vercel Cron, Cloudflare Triggers): meno pezzi se sei già sul piano giusto.
- VPS con systemd timer: controllo totale del clock, costo fisso.
Disclosure: @snowinch/githubcron
Snowinch mantiene il pacchetto open source @snowinch/githubcron, che automatizza la creazione di workflow e handler tipizzati (es. integrazione Next.js App Router) partendo dallo stesso pattern sopra. Non è obbligatorio: il YAML manuale è già produzione-ready se preferisci zero dipendenze.
Esempio d’uso libreria (dopo pnpm add @snowinch/githubcron):
import { ServerlessCron } from '@snowinch/githubcron'
export const cron = new ServerlessCron({
secret: process.env.GITHUBCRON_SECRET,
baseUrl: process.env.NEXT_PUBLIC_APP_URL,
})
cron.job('send-daily-emails', {
schedule: '0 9 * * *',
handler: async () => {
const result = await sendDailyEmails()
return { sent: result.length }
},
})
// app/api/cron/[job]/route.ts — Next.js App Router
export const POST = cron.nextjs.appRouter()
Poi npx githubcron generate per materializzare i file workflow in .github/workflows/.
Sintesi
- Implementa POST + secret nell’app.
- Aggiungi workflow on.schedule con
curl. - Valida con workflow_dispatch prima di affidarti al solo cron.
- Valuta
@snowinch/githubcronsolo se vuoi generare quel boilerplate in modo ripetibile su più progetti.
Non serve promettere “risparmio €X/anno”: il criterio è operativo — il job gira in modo osservabile e dentro i limiti di GitHub e del tuo host.