[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-it-githubcron-cron-job-serverless-gratuiti":3},{"id":4,"title":5,"author":6,"body":7,"cover":714,"description":715,"draft":716,"extension":717,"locale":718,"meta":719,"navigation":217,"path":720,"publishedAt":721,"seo":722,"seoTitle":723,"slug":724,"stem":725,"tags":726,"__hash__":732},"blog\u002Fit\u002Fblog\u002Fgithubcron-cron-job-serverless-gratuiti.md","Cron senza server: GitHub Actions che chiama la tua API (scheduled webhook)","Manuel Tardivo",{"type":8,"value":9,"toc":704},"minimark",[10,29,52,62,67,121,128,133,140,303,324,330,334,377,381,401,408,424,442,449,644,654,658,693,700],[11,12,13,17,18,21,22,25,26,28],"p",{},[14,15,16],"strong",{},"Problema che risolvi:"," hai un’app su ",[14,19,20],{},"Vercel \u002F Netlify \u002F Cloudflare"," e ti serve un job periodico (report, sync, reminder) ",[14,23,24],{},"senza"," VPS sempre acceso e ",[14,27,24],{}," subito AWS EventBridge.",[11,30,31,34,35,38,39,43,44,47,48,51],{},[14,32,33],{},"Cosa fai in ~20 minuti:"," un workflow ",[14,36,37],{},"GitHub Actions"," su ",[40,41,42],"code",{},"schedule"," che fa ",[14,45,46],{},"POST"," al tuo endpoint con un ",[14,49,50],{},"secret"," condiviso.",[11,53,54,57,58,61],{},[14,55,56],{},"Come sai che funziona:"," nella tab ",[14,59,60],{},"Actions"," vedi run verdi e nei log del server la richiesta autenticata.",[63,64,66],"h2",{"id":65},"il-pattern-scheduled-webhook","Il pattern “scheduled webhook”",[68,69,70,89,104,115],"ol",{},[71,72,73,76,77,80,81,88],"li",{},[14,74,75],{},"GitHub"," esegue il workflow all’ora definita (",[40,78,79],{},"cron"," in UTC — vedi ",[82,83,87],"a",{"href":84,"rel":85},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Fusing-workflows\u002Fevents-that-trigger-workflows#schedule",[86],"nofollow","Events that trigger workflows",").",[71,90,91,92,95,96,99,100,103],{},"Un solo step fa ",[40,93,94],{},"curl"," (o ",[40,97,98],{},"fetch"," in Node) verso ",[40,101,102],{},"https:\u002F\u002Ftua-app.example\u002Fapi\u002Fcron\u002F...",".",[71,105,106,107,110,111,114],{},"L’app verifica header tipo ",[40,108,109],{},"Authorization: Bearer …"," confrontando con ",[40,112,113],{},"CRON_SECRET"," in env.",[71,116,117,118,103],{},"L’handler esegue la logica (DB, email, ecc.) e risponde ",[40,119,120],{},"200",[11,122,123,124,127],{},"La logica resta ",[14,125,126],{},"nell’app",": Actions è solo un sveglia affidabile quanto basta per molti casi d’uso interni.",[129,130,132],"h3",{"id":131},"workflow-minimale-yaml","Workflow minimale (YAML)",[11,134,135,136,139],{},"Salva in ",[40,137,138],{},".github\u002Fworkflows\u002Fcron-daily.yml"," (adatta URL e nome segreto):",[141,142,147],"pre",{"className":143,"code":144,"language":145,"meta":146,"style":146},"language-yaml shiki shiki-themes github-light github-dark","name: daily-cron-webhook\non:\n  schedule:\n    - cron: '0 9 * * *' # ogni giorno 09:00 UTC — verifica fuso vs business\n  workflow_dispatch: # permette run manuale dalla UI\n\njobs:\n  call-app:\n    runs-on: ubuntu-latest\n    steps:\n      - name: POST \u002Fapi\u002Fcron\u002Fdaily\n        run: |\n          curl -fsS -X POST \"${{ secrets.APP_CRON_URL }}\" \\\n            -H \"Authorization: Bearer ${{ secrets.CRON_SECRET }}\" \\\n            -H \"Content-Type: application\u002Fjson\" \\\n            -d '{}'\n","yaml","",[40,148,149,166,176,184,201,212,219,227,235,246,254,267,279,285,291,297],{"__ignoreMap":146},[150,151,154,158,162],"span",{"class":152,"line":153},"line",1,[150,155,157],{"class":156},"s9eBZ","name",[150,159,161],{"class":160},"sVt8B",": ",[150,163,165],{"class":164},"sZZnC","daily-cron-webhook\n",[150,167,169,173],{"class":152,"line":168},2,[150,170,172],{"class":171},"sj4cs","on",[150,174,175],{"class":160},":\n",[150,177,179,182],{"class":152,"line":178},3,[150,180,181],{"class":156},"  schedule",[150,183,175],{"class":160},[150,185,187,190,192,194,197],{"class":152,"line":186},4,[150,188,189],{"class":160},"    - ",[150,191,79],{"class":156},[150,193,161],{"class":160},[150,195,196],{"class":164},"'0 9 * * *'",[150,198,200],{"class":199},"sJ8bj"," # ogni giorno 09:00 UTC — verifica fuso vs business\n",[150,202,204,207,209],{"class":152,"line":203},5,[150,205,206],{"class":156},"  workflow_dispatch",[150,208,161],{"class":160},[150,210,211],{"class":199},"# permette run manuale dalla UI\n",[150,213,215],{"class":152,"line":214},6,[150,216,218],{"emptyLinePlaceholder":217},true,"\n",[150,220,222,225],{"class":152,"line":221},7,[150,223,224],{"class":156},"jobs",[150,226,175],{"class":160},[150,228,230,233],{"class":152,"line":229},8,[150,231,232],{"class":156},"  call-app",[150,234,175],{"class":160},[150,236,238,241,243],{"class":152,"line":237},9,[150,239,240],{"class":156},"    runs-on",[150,242,161],{"class":160},[150,244,245],{"class":164},"ubuntu-latest\n",[150,247,249,252],{"class":152,"line":248},10,[150,250,251],{"class":156},"    steps",[150,253,175],{"class":160},[150,255,257,260,262,264],{"class":152,"line":256},11,[150,258,259],{"class":160},"      - ",[150,261,157],{"class":156},[150,263,161],{"class":160},[150,265,266],{"class":164},"POST \u002Fapi\u002Fcron\u002Fdaily\n",[150,268,270,273,275],{"class":152,"line":269},12,[150,271,272],{"class":156},"        run",[150,274,161],{"class":160},[150,276,278],{"class":277},"szBVR","|\n",[150,280,282],{"class":152,"line":281},13,[150,283,284],{"class":164},"          curl -fsS -X POST \"${{ secrets.APP_CRON_URL }}\" \\\n",[150,286,288],{"class":152,"line":287},14,[150,289,290],{"class":164},"            -H \"Authorization: Bearer ${{ secrets.CRON_SECRET }}\" \\\n",[150,292,294],{"class":152,"line":293},15,[150,295,296],{"class":164},"            -H \"Content-Type: application\u002Fjson\" \\\n",[150,298,300],{"class":152,"line":299},16,[150,301,302],{"class":164},"            -d '{}'\n",[304,305,306,315],"ul",{},[71,307,308,311,312],{},[40,309,310],{},"APP_CRON_URL",": es. ",[40,313,314],{},"https:\u002F\u002Fwww.example.com\u002Fapi\u002Fcron\u002Fdaily",[71,316,317,319,320,323],{},[40,318,113],{},": stringa lunga random; ",[14,321,322],{},"stesso valore"," in env produzione dell’app.",[11,325,326,329],{},[14,327,328],{},"Sicurezza:"," non loggare il secret; ruotalo se esposto; limita l’endpoint a operazioni idempotenti dove possibile.",[63,331,333],{"id":332},"limiti-reali-da-non-sottovalutare","Limiti reali (da non sottovalutare)",[68,335,336,349,355,368],{},[71,337,338,341,342,344,345,348],{},[14,339,340],{},"Jitter e ritardo:"," ",[40,343,42],{}," non è real-time; GitHub documenta che l’esecuzione può ",[14,346,347],{},"ritardare"," in periodi di alto carico. Non usare questo pattern per trading, aste o pagamenti con finestra di secondi.",[71,350,351,354],{},[14,352,353],{},"Minuti Actions:"," i piani gratuiti hanno un budget minuti\u002Fmese — conta durata job × frequenza. Un job di 5 min ogni ora può esaurire rapidamente il free tier.",[71,356,357,360,361,364,365,103],{},[14,358,359],{},"Timeout dell’hosting:"," il POST deve finire dentro il limite della tua funzione (es. ",[14,362,363],{},"10s"," su Vercel Hobby). Se il lavoro è lungo: accoda un job interno (DB\u002Fqueue) e rispondi subito ",[40,366,367],{},"202",[71,369,370,373,374,376],{},[14,371,372],{},"Repo pubblico:"," il file YAML è visibile; i ",[14,375,50],{}," no, ma evita di mettere URL con token in chiaro nel YAML.",[63,378,380],{"id":379},"alternative-quando-il-pattern-non-basta","Alternative quando il pattern non basta",[304,382,383,389,395],{},[71,384,385,388],{},[14,386,387],{},"Queue + worker"," (BullMQ, Cloud Tasks, SQS): per carichi lunghi o retry fine.",[71,390,391,394],{},[14,392,393],{},"Cron del provider"," (Vercel Cron, Cloudflare Triggers): meno pezzi se sei già sul piano giusto.",[71,396,397,400],{},[14,398,399],{},"VPS con systemd timer:"," controllo totale del clock, costo fisso.",[63,402,404,405],{"id":403},"disclosure-snowinchgithubcron","Disclosure: ",[40,406,407],{},"@snowinch\u002Fgithubcron",[11,409,410,411,415,416,419,420,423],{},"Snowinch mantiene il pacchetto open source ",[14,412,413],{},[40,414,407],{},", che ",[14,417,418],{},"automatizza"," la creazione di workflow e handler tipizzati (es. integrazione ",[14,421,422],{},"Next.js App Router",") partendo dallo stesso pattern sopra. Non è obbligatorio: il YAML manuale è già produzione-ready se preferisci zero dipendenze.",[304,425,426,435],{},[71,427,428],{},[82,429,432,433],{"href":430,"rel":431},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@snowinch\u002Fgithubcron",[86],"npm ",[40,434,407],{},[71,436,437],{},[82,438,441],{"href":439,"rel":440},"https:\u002F\u002Fgithub.com\u002Fsnowinch\u002Fsnowinch-tools\u002Ftree\u002Fmain\u002Fpackages\u002Fgithubcron",[86],"Sorgente \u002F documentazione",[11,443,444,445,448],{},"Esempio d’uso libreria (dopo ",[40,446,447],{},"pnpm add @snowinch\u002Fgithubcron","):",[141,450,454],{"className":451,"code":452,"language":453,"meta":146,"style":146},"language-typescript shiki shiki-themes github-light github-dark","import { ServerlessCron } from '@snowinch\u002Fgithubcron'\n\nexport const cron = new ServerlessCron({\n  secret: process.env.GITHUBCRON_SECRET,\n  baseUrl: process.env.NEXT_PUBLIC_APP_URL,\n})\n\ncron.job('send-daily-emails', {\n  schedule: '0 9 * * *',\n  handler: async () => {\n    const result = await sendDailyEmails()\n    return { sent: result.length }\n  },\n})\n\n\u002F\u002F app\u002Fapi\u002Fcron\u002F[job]\u002Froute.ts — Next.js App Router\nexport const POST = cron.nextjs.appRouter()\n","typescript",[40,455,456,470,474,498,509,519,524,528,545,554,573,592,606,611,615,619,624],{"__ignoreMap":146},[150,457,458,461,464,467],{"class":152,"line":153},[150,459,460],{"class":277},"import",[150,462,463],{"class":160}," { ServerlessCron } ",[150,465,466],{"class":277},"from",[150,468,469],{"class":164}," '@snowinch\u002Fgithubcron'\n",[150,471,472],{"class":152,"line":168},[150,473,218],{"emptyLinePlaceholder":217},[150,475,476,479,482,485,488,491,495],{"class":152,"line":178},[150,477,478],{"class":277},"export",[150,480,481],{"class":277}," const",[150,483,484],{"class":171}," cron",[150,486,487],{"class":277}," =",[150,489,490],{"class":277}," new",[150,492,494],{"class":493},"sScJk"," ServerlessCron",[150,496,497],{"class":160},"({\n",[150,499,500,503,506],{"class":152,"line":186},[150,501,502],{"class":160},"  secret: process.env.",[150,504,505],{"class":171},"GITHUBCRON_SECRET",[150,507,508],{"class":160},",\n",[150,510,511,514,517],{"class":152,"line":203},[150,512,513],{"class":160},"  baseUrl: process.env.",[150,515,516],{"class":171},"NEXT_PUBLIC_APP_URL",[150,518,508],{"class":160},[150,520,521],{"class":152,"line":214},[150,522,523],{"class":160},"})\n",[150,525,526],{"class":152,"line":221},[150,527,218],{"emptyLinePlaceholder":217},[150,529,530,533,536,539,542],{"class":152,"line":229},[150,531,532],{"class":160},"cron.",[150,534,535],{"class":493},"job",[150,537,538],{"class":160},"(",[150,540,541],{"class":164},"'send-daily-emails'",[150,543,544],{"class":160},", {\n",[150,546,547,550,552],{"class":152,"line":237},[150,548,549],{"class":160},"  schedule: ",[150,551,196],{"class":164},[150,553,508],{"class":160},[150,555,556,559,561,564,567,570],{"class":152,"line":248},[150,557,558],{"class":493},"  handler",[150,560,161],{"class":160},[150,562,563],{"class":277},"async",[150,565,566],{"class":160}," () ",[150,568,569],{"class":277},"=>",[150,571,572],{"class":160}," {\n",[150,574,575,578,581,583,586,589],{"class":152,"line":256},[150,576,577],{"class":277},"    const",[150,579,580],{"class":171}," result",[150,582,487],{"class":277},[150,584,585],{"class":277}," await",[150,587,588],{"class":493}," sendDailyEmails",[150,590,591],{"class":160},"()\n",[150,593,594,597,600,603],{"class":152,"line":269},[150,595,596],{"class":277},"    return",[150,598,599],{"class":160}," { sent: result.",[150,601,602],{"class":171},"length",[150,604,605],{"class":160}," }\n",[150,607,608],{"class":152,"line":281},[150,609,610],{"class":160},"  },\n",[150,612,613],{"class":152,"line":287},[150,614,523],{"class":160},[150,616,617],{"class":152,"line":293},[150,618,218],{"emptyLinePlaceholder":217},[150,620,621],{"class":152,"line":299},[150,622,623],{"class":199},"\u002F\u002F app\u002Fapi\u002Fcron\u002F[job]\u002Froute.ts — Next.js App Router\n",[150,625,627,629,631,634,636,639,642],{"class":152,"line":626},17,[150,628,478],{"class":277},[150,630,481],{"class":277},[150,632,633],{"class":171}," POST",[150,635,487],{"class":277},[150,637,638],{"class":160}," cron.nextjs.",[150,640,641],{"class":493},"appRouter",[150,643,591],{"class":160},[11,645,646,647,650,651,103],{},"Poi ",[40,648,649],{},"npx githubcron generate"," per materializzare i file workflow in ",[40,652,653],{},".github\u002Fworkflows\u002F",[63,655,657],{"id":656},"sintesi","Sintesi",[68,659,660,667,676,683],{},[71,661,662,663,666],{},"Implementa ",[14,664,665],{},"POST + secret"," nell’app.",[71,668,669,670,673,674,103],{},"Aggiungi workflow ",[14,671,672],{},"on.schedule"," con ",[40,675,94],{},[71,677,678,679,682],{},"Valida con ",[14,680,681],{},"workflow_dispatch"," prima di affidarti al solo cron.",[71,684,685,686,688,689,692],{},"Valuta ",[40,687,407],{}," solo se vuoi ",[14,690,691],{},"generare"," quel boilerplate in modo ripetibile su più progetti.",[11,694,695,696,699],{},"Non serve promettere “risparmio €X\u002Fanno”: il criterio è operativo — ",[14,697,698],{},"il job gira in modo osservabile e dentro i limiti"," di GitHub e del tuo host.",[701,702,703],"style",{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":146,"searchDepth":168,"depth":168,"links":705},[706,709,710,711,713],{"id":65,"depth":168,"text":66,"children":707},[708],{"id":131,"depth":178,"text":132},{"id":332,"depth":168,"text":333},{"id":379,"depth":168,"text":380},{"id":403,"depth":168,"text":712},"Disclosure: @snowinch\u002Fgithubcron",{"id":656,"depth":168,"text":657},"\u002Fblog\u002F18_githubcron_cron_job_serverless_gratuiti_1x.webp","Cron serverless: workflow GitHub Actions su schedule, POST autenticato verso l'app, limiti di precisione e minuti. Pattern, non un prodotto da gallery.",false,"md","it",{},"\u002Fit\u002Fblog\u002Fgithubcron-cron-job-serverless-gratuiti","2026-03-08",{"title":5,"description":715},"Cron senza server: GitHub Actions e API","githubcron-cron-job-serverless-gratuiti","it\u002Fblog\u002Fgithubcron-cron-job-serverless-gratuiti",[727,728,729,730,731],"githubcron","cron job serverless","github actions","automazione","open source","HXcYrqMWF52xGJVhVde3Gkyekc5-t0UqhqB2qBHB_Gc"]