구독 요금제 한도를 피해 AI 워커를 새벽에만 돌리기
AI 분석을 구독 요금제로 자동화하면 짧은 시간에 몰아 쓸 때 한도에 걸립니다. 작업을 큐에 쌓아 두고 새벽 시간창에서만, 건당 간격을 두고 천천히 처리해 비용을 거의 0으로 유지하는 워커 설계를 cron·시간창·페이싱 코드까지 정리합니다.
혼자 회사 시스템을 돌리다 보면 AI에 맡기고 싶은 일이 점점 늘어납니다. 수집한 글을 요약하고, 분류하고, 리포트로 묶는 일까지. 그런데 이걸 구독 요금제로 자동화하면 금방 벽에 부딪힙니다. 짧은 시간에 작업을 몰아넣는 순간 한도에 걸리기 때문입니다.
선택지는 보통 두 갈래로 보입니다. 종량제 API로 갈아타거나(비용 증가), 자동화를 포기하거나(작업량 축소). 둘 다 내키지 않아서 세 번째 길을 택했습니다. 작업을 미뤄 두고, 사람이 안 쓰는 새벽에, 천천히 흘려보내는 것입니다.
구독 요금제가 한도에 걸리는 이유
구독 요금제는 무제한처럼 보이지만 burst에 약한 구조입니다. 한가하게 쓰면 한도를 거의 못 느끼지만, 자동화가 짧은 시간에 수십 건을 던지면 속도 제한이나 일시 차단이 걸립니다. 사람이 손으로 쓰는 패턴과 봇이 몰아치는 패턴이 다르기 때문입니다.
그래서 목표를 바꿨습니다. “빨리 많이”가 아니라 “느리게, 사람 안 쓰는 시간에”. 분석 결과가 실시간으로 필요하지 않다면 이 트레이드는 충분히 남는 장사입니다.
전체 구조
흐름은 단순합니다.
- 낮 — 큐에 쌓기. 수집기나 다른 작업이 분석할 거리를 만들 때마다, 바로 처리하지 않고 공용 작업 큐에
PENDING으로 넣어 둡니다. - 새벽 2시 — cron이 워커를 깨움.
- 2~5시 — 시간창 안에서만 처리. 워커는 큐에서 하나씩 꺼내 처리하고, 매 건 사이에 무작위로 쉬었다가, 창 밖 시간이 되면 스스로 종료합니다.
낮에 쌓인 일이 자는 동안 처리되고, 아침엔 결과만 받습니다.
cron — 새벽에 한 번 깨우기
0 2 * * * /bin/bash ~/llm-worker/scripts/run-windowed.sh >> ~/llm-worker/llm-worker.log 2>&1
매일 새벽 2시에 워커 스크립트를 실행하고 로그를 파일에 남깁니다. 여기서 첫 번째 함정이 cron의 최소 PATH입니다. cron은 로그인 셸과 달리 PATH가 거의 비어 있어, node나 claude 같은 실행 파일을 못 찾고 조용히 실패합니다. 그래서 스크립트 맨 위에서 PATH를 직접 채워 줍니다.
시간창 래퍼
#!/usr/bin/env bash
set -euo pipefail
# cron은 PATH가 최소라 node·claude를 못 찾는다 → 보강
export PATH="/usr/local/bin:/opt/homebrew/bin:$HOME/.local/bin:$PATH"
cd "$(dirname "$0")/.."
EXIT_WHEN_OUT_OF_WINDOW=true \
PROCESS_WINDOW_START_HOUR=2 \
PROCESS_WINDOW_END_HOUR=5 \
node --env-file=.env worker.mjs
처리 시간창(2~5시)과 “창 밖이면 종료” 옵션을 환경 변수로 넘기고, 실제 처리는 worker.mjs가 합니다. 인증은 API 키가 아니라 구독 세션을 그대로 씁니다. 그래서 추가 비용 없이 구독 한도 안에서 도는 겁니다.
워커 루프 — 천천히, 창 안에서만
핵심 로직만 추리면 이렇게 생겼습니다.
const START = Number(process.env.PROCESS_WINDOW_START_HOUR); // 2
const END = Number(process.env.PROCESS_WINDOW_END_HOUR); // 5
const inWindow = () => {
const h = new Date().getHours();
return h >= START && h < END;
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
while (inWindow()) {
const job = await claimNextPendingJob(); // 큐에서 하나 꺼내기
if (!job) { await sleep(60_000); continue } // 없으면 잠깐 쉬고 재확인
await process(job); // 구독 세션으로 처리
// 건당 20~90초 무작위 간격 = burst 회피
await sleep((20 + Math.random() * 70) * 1000);
}
// 창 밖이면 루프 종료 → 프로세스 exit
한도를 피하는 장치는 두 개입니다. 창(inWindow)으로 처리 시간대를 좁히고, 건당 무작위 간격(sleep)으로 요청이 몰리지 않게 흩뿌립니다. 빠르게 끝내는 게 목적이 아니라, 티 안 나게 천천히 흘려보내는 게 목적입니다.
트레이드오프 — 지연을 비용으로 바꾼 것
이 설계의 비용은 지연입니다. 낮에 들어온 분석은 그날 밤에야 처리되니, 빠르면 몇 시간, 늦으면 하루 정도 늦습니다. 대신 얻는 건 거의 0원의 운영비입니다. 종량제 API를 안 쓰고 구독 한도 안에서만 돌기 때문입니다.
그래서 이 방식은 미뤄도 되는 무거운 작업에만 맞습니다. 요약·분류·리포트처럼 하루 늦어도 괜찮은 배치는 새벽으로, 사용자가 화면에서 기다리는 실시간 요청은 따로 즉시 처리. 이렇게 나누면 대부분의 무거운 일을 공짜 선에서 끝낼 수 있습니다.
혼자 운영할수록 이런 작은 설계 하나가 한 달 단위로 쌓입니다. 빨리 처리하고 싶은 본능을 한 번 누르고 “이건 새벽에 천천히 해도 되는 일인가?”를 먼저 묻는 것. 그 한 줄이 비용을 0에 가깝게 유지해 줍니다.
자주 묻는 질문
실시간으로 바로 필요한 작업도 이렇게 미루나요?
아니요. 이 방식은 급하지 않은 배치 작업(요약·분류·리포트 생성)에만 씁니다. 사용자가 화면에서 기다리는 실시간 요청은 종량제 API로 즉시 처리하고, 비용이 들어도 그쪽은 따로 둡니다. 미뤄도 되는 무거운 작업만 새벽으로 몰아넣는 게 핵심입니다.
왜 하필 새벽 2~5시인가요?
두 가지 이유입니다. 첫째, 본인이 구독을 쓰지 않는 시간이라 같은 계정의 한도를 두고 경쟁하지 않습니다. 둘째, 짧은 시간에 몰아 쓰면 구독 요금제가 속도 제한이나 페널티를 거는데, 사람이 안 쓰는 시간에 천천히 흘려보내면 그 트리거를 피하기 쉽습니다.
큐에 작업이 쌓이는 속도가 처리 속도보다 빠르면 어떻게 하나요?
시간창 길이와 건당 간격으로 처리량을 조절합니다. 유입이 더 많으면 창을 늘리거나(2~6시) 간격을 줄입니다. 그래도 약간의 지연은 남는데, 요약·분석은 하루 늦어도 괜찮은 작업이라 그 지연을 감수하는 설계입니다.