매일 아침 일정을 브리핑해주는 텔레그램 봇을 만들었습니다. GitHub Actions의 schedule 기능으로 매일 새벽 5시에 돌게 걸어두고, 이제 아침마다 아침마다 그날 할 일이 뭐가 있는지 파악하고 하루를 미리 그려볼 수 있겠구나 싶었습니다. 그런데 메시지는 오전 5시가 아니라 매일 6시가 넘어서야 도착했습니다.
GitHub Actions 워크플로는 아래와 같습니다.
# .github/workflows/daily-briefing.yml
name: Daily briefing
on:
schedule:
# 매일 한국 시간(KST) 새벽 5시 정각에 실행
# GitHub Actions는 UTC로 동작하므로 UTC 20:00 = KST 다음날 05:00
- cron: "0 20 * * *"
workflow_dispatch:
#...
GitHub Actions의 cron은 UTC 기준이니까 UTC 20:00은 KST로 다음날 오전 5시가 맞습니다.
그런데 실제로 메시지가 도착한 시각과 Actions 실행 이력을 일주일치 대조해보니 아래와 같았습니다.
[7/5~7/11 까지의 Actions 실행 이력]
| 날짜 | 기대 시각 | Actions run 시작 | 실행 소요 | 텔레그램 수신 | 지연 |
|---|---|---|---|---|---|
| 7/5 | 05:00 | 06:06 | 19s | 06:06 | +66분 |
| 7/6 | 05:00 | 06:11 | 18s | 06:11 | +71분 |
| 7/7 | 05:00 | 06:42 | 16s | 06:42 | +102분 |
| 7/8 | 05:00 | 06:35 | 24s | 06:35 | +95분 |
| 7/9 | 05:00 | 06:17 | 20s | 06:17 | +77분 |
| 7/10 | 05:00 | 06:35 | 23s | 06:35 | +95분 |
| 7/11 | 05:00 | 06:13 | 19s | 06:13 | +73분 |
[Actions 실행 이력 스크린샷]

Actions 실행 이력을 보니 지연 폭이 매일 다르다는 걸 볼 수 있습니다. 66분, 71분, 102분 등 들쭉날쭉하고 하루도 제시간에 실행된 적이 없습니다.
실행 시간은 평균 20초로 멀쩡했고, run 시작 시각과 텔레그램 수신 시각이 일치하는 걸 보니 스케줄이 지연되는 거라고 생각했습니다.
GitHub Actions 스케줄이 왜 지연되는지 확인하기 위해 공식 문서를 읽어 봤습니다.
GitHub Docs의 Events that trigger workflows — schedule 섹션 상단 Note에 아래와 같은 내용이 있습니다.
schedule 이벤트는 GitHub Actions 워크플로 실행 부하가 높은 기간에 지연될 수 있다. 부하가 높은 시간대에는 매 정시(시각의 시작)가 포함된다. 부하가 충분히 높으면 대기 중인 일부 작업이 드롭될 수도 있다. 지연 가능성을 줄이려면 정시가 아닌 다른 분(minute)에 실행되도록 예약하라. (요약 번역)
GitHub Actions의 cron은 “이 시각에 실행해준다”는 약속이 아니라 “이 시각에 큐에 넣고, 여유가 되면 실행한다”는 방식입니다. 정각에 cron을 걸어두는 사람이 많아 매 정시마다 큐가 몰리고, 제 워크플로도 그 혼잡에 밀려 매일 1시간 넘게 늦고 있었던 겁니다.
정리: 정시는 부하가 높은 시간대이기 때문에 제 시간에 스케줄이 실행되지 않을 수 있다.
일정 브리핑 메시지를 오전 5시에 받아보고 싶기 때문에 해결 방법을 찾아보았습니다. 개인용 봇이라 무료로 해결할 수 있는 방법만 추렸습니다.
공식 문서에 언급되어 있는 방법으로 정시가 아닌 다른 분으로 시간을 옮겨 지연 시간을 최소화합니다.
0 20 * * *(KST 05:00)을 예를 들어 23 19 * * *(KST 04:23)처럼 어중간한 분으로 옮기면 정각 혼잡을 피할 수 있습니다.
workflow yml 파일에 이미 있는 workflow_dispatch 트리거를 활용하는 방법입니다. cron-job.org나 Cloudflare Workers 같은 외부 서비스가 매일 정해진 시각에 GitHub API를 호출해서 워크플로를 직접 깨웁니다. workflow_dispatch는 스케줄 큐를 타지 않고 이벤트 즉시 실행되기 때문에 지연되지 않을 가능성이 높습니다.
GitHub Actions를 떠나서, 항상 켜져 있는 컴퓨터에서 텔레그램 봇을 실행하는 방법입니다. 컴퓨터 운영체제에 내장된 예약 실행 기능(crontab)을 이용합니다.
저는 5시 정각에 가깝게 받고 싶어서 방법 1로는 부족했고, 그렇다고 라즈베리파이 같은 컴퓨터를 텔레그램 봇 하나 때문에 상시로 돌리는 건 과하다고 생각했습니다. 그래서 방법 2(외부 cron 서비스에서 workflow_dispatch API 호출)을 사용하기로 결정했습니다. 외부 서비스로는 Cloudflare Workers 무료 플랜을 사용하기로 했습니다.
전체 구조는 아래와 같습니다.
Cloudflare Cron Trigger (UTC 20:00 = KST 05:00 정각 발화)
→ Worker의 scheduled 핸들러 실행
→ GitHub API request: POST .../workflows/daily-briefing.yml/dispatches
→ GitHub이 workflow_dispatch 이벤트로 run 즉시 생성 (스케줄 큐 우회)
→ briefing.py 실행
→ 텔레그램 수신
외부에서 GitHub API로 워크플로를 깨우기 위해서는 토큰이 필요합니다.
Settings → Developer settings → Personal access tokens → Fine-grained tokens에서 토큰을 발급합니다.
워크플로 yml에서는 schedule 블록을 제거했습니다.
남겨두면 Cloudflare cron trigger로 1회 + GitHub Actions Schedule Event 1회 하루에 총 2번 브리핑이 오게 됩니다.
# .github/workflows/daily-briefing.yml
name: Daily briefing
on:
workflow_dispatch:
#...
dash.cloudflare.com에서 Workers & Pages → Create → Hello World 템플릿으로 Worker를 하나 만들고, Edit code로 들어가 기본 코드를 아래로 통째로 교체했습니다.
export default {
async scheduled(event, env, ctx) {
ctx.waitUntil(triggerBriefing(env));
},
};
async function triggerBriefing(env) {
const url = `https://api.github.com/repos/${env.GITHUB_OWNER}/${env.GITHUB_REPO}/actions/workflows/daily-briefing.yml/dispatches`;
const res = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${env.GITHUB_TOKEN}`,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
// GitHub API는 User-Agent 헤더가 없으면 403을 반환한다.
// Workers의 fetch는 기본 User-Agent를 붙이지 않으므로 반드시 명시.
"User-Agent": "daily-briefing-trigger",
"Content-Type": "application/json",
},
body: JSON.stringify({ ref: "main" }),
});
// 성공 시 GitHub은 204 No Content를 반환
if (res.status === 204) {
console.log("workflow_dispatch OK");
return;
}
// 실패하면 텔레그램으로 직접 알림을 보낸다
const body = await res.text();
console.error(`workflow_dispatch failed: HTTP ${res.status} - ${body}`);
if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_CHAT_ID) {
await fetch(
`https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: env.TELEGRAM_CHAT_ID,
text: `⚠️ 데일리 브리핑 트리거 실패\nHTTP ${res.status}\n${body.slice(0, 300)}`,
}),
}
);
}
}
코드 배포 후 Settings → Variables and Secrets에서 변수를 등록했습니다. 변수는 GITHUB_TOKEN(Secret), GITHUB_OWNER, GITHUB_REPO, 그리고 실패 알림용 TELEGRAM_BOT_TOKEN(Secret), TELEGRAM_CHAT_ID 다섯 개입니다.
마지막으로 Settings → Triggers → Cron Triggers에 0 20 * * *를 등록했습니다.
UTC 20:00까지 기다릴 수는 없으니, cron을 잠시 */5 * * * *(5분마다)로 바꿔서 테스트를 진행했습니다.
워크플로가 두 번 실행되고 브리핑 메시지까지 도착하는 것을 확인한 뒤, cron을 원래대로 돌려놓았습니다.

아직 테스트까지만 해보고 실제로 새벽 5시에 일정 브리핑 메시지가 오는지 확인은 못했습니다. 이 글을 쓰면서 같이 고쳤기 때문입니다. 내일 아침에는 처음으로 제시간에 온 일정 브리핑을 받아볼 수 있을 것 같습니다. 결과는 추후에 덧붙이도록 하겠습니다.
잘 읽었습니다! 새벽 5시에 실제 브리핑 메시지가 원하는 대로 도착하는 지 궁금해지네요.
Github Actions에서 크론잡이 지연되어 Cloudflare로 옮겼다면 Github Actions의 용도가 사라졌다고 생각하는데 Cloudflare에서 직접 메시지를 발송하는 것이 아닌 Github Acitons를 수동으로 트리거해서 발송하는 이유가 따로 있을지 궁금합니다 :)