Workflows 사용량 경보를 나타내는 계기판과 상태 카드가 연결된 일러스트

Cloudflare Workflows stepCount 비용 경보: 8월 10일 과금 후 사용량 확인하기

Cloudflare Workflows stepCount 비용 경보가 월 포함량의 160%를 가리킨다고 해서 곧바로 초과 과금이 발생한 것은 아니다. workflowsAdaptivestepCount는 사용량 증분이 아니라 한 인스턴스 안의 단계 순번이다. 같은 단계에서 발생한 여러 lifecycle event 행을 모두 더하면 50%인 관찰값도 160%로 부풀 수 있다.

Workflows step·storage 과금은 현재 공식 pricing 기준 2026년 8월 10일부터 적용된다. 이 글은 8월 11일 공개 문서와 Linux 7.0.0-1009-aws x86_64·Node.js v18.19.1 격리 fixture로 집계 실패와 경보 계산을 확인한 결과를 다룬다. Cloudflare 실계정, 대시보드, invoice에는 접속하지 않았으므로 GraphQL 값은 조기 경보 신호로만 사용하고 청구량은 별도로 대조해야 한다.

20초 핵심 요약

  • 무엇: stepCount lifecycle 행의 중복 합산을 피하고 Workflows 사용량 경보를 만드는 방법이다.
  • 왜: 순번을 사용량처럼 더하면 50% 사용을 160% 초과로 오판해 불필요한 실행 제한을 걸 수 있다.
  • 어떻게: 인스턴스별 최종 순번으로 근사치를 만들고 50·80·100% 상태를 계산한 뒤, 오류·retry·invoice를 서로 다른 신호로 확인한다.

50%가 160%로 보였다면 행 합산부터 의심한다

공개 schema 형태를 본뜬 두 인스턴스의 lifecycle 행에 단순 합계를 적용하자 다음처럼 실패했다. 같은 단계의 STEP_START, ATTEMPT_*, STEP_SUCCESS가 각각 한 행으로 나타나는데, 각 행에 들어 있는 동일한 순번을 모두 소비량으로 더한 결과다.

node -e '
const rows = [
  { instanceId: "a", eventType: "STEP_START", stepCount: 100000 },
  { instanceId: "a", eventType: "ATTEMPT_START", stepCount: 100000 },
  { instanceId: "a", eventType: "ATTEMPT_SUCCESS", stepCount: 100000 },
  { instanceId: "a", eventType: "STEP_SUCCESS", stepCount: 100000 },
  { instanceId: "b", eventType: "STEP_START", stepCount: 150000 },
  { instanceId: "b", eventType: "STEP_SUCCESS", stepCount: 150000 },
  { instanceId: "b", eventType: "ROLLBACK_START", stepCount: 100000 }
];
const wrongRowSum = rows.reduce((sum, row) => sum + row.stepCount, 0);
const maxima = new Map();
for (const row of rows) maxima.set(row.instanceId, Math.max(maxima.get(row.instanceId) || 0, row.stepCount));
const correctedInstanceMaxSum = [...maxima.values()].reduce((sum, value) => sum + value, 0);
const retryEvents = rows.filter((row) => row.eventType.startsWith("ATTEMPT_")).length;
console.log(JSON.stringify({ wrongRowSum, wrongBudgetPct: wrongRowSum / 500000 * 100 }));
console.log(JSON.stringify({ correctedInstanceMaxSum, correctedBudgetPct: correctedInstanceMaxSum / 500000 * 100, retryEvents }));
console.error("ERROR stepCount is an ordinal dimension; summing lifecycle rows double-counts the same steps");
process.exit(2);
'; printf 'exit_code=%s\n' "$?"

{"wrongRowSum":800000,"wrongBudgetPct":160}
{"correctedInstanceMaxSum":250000,"correctedBudgetPct":50,"retryEvents":2}
ERROR stepCount is an ordinal dimension; summing lifecycle rows double-counts the same steps
exit_code=2

lifecycle 행 합산이 50%를 160%로 오판하고 exit 2로 끝나는 명령과 출력

오류의 출발점은 이름이 아니라 값의 의미다. 공식 Workflows metrics 문서stepCount를 특정 instanceId 안의 step number로 정의하고 lifecycle event 종류를 별도로 제시한다. 한 step에는 시작, 시도, 성공 또는 실패와 같은 여러 이벤트가 생길 수 있으므로 raw 행의 stepCount 합은 월간 사용 step 수가 아니다.

fixture에서는 각 인스턴스의 최대 stepCount를 더해 250,000을 얻었다. 이 수정은 중복 합산 오류를 제거하지만 Cloudflare의 공식 billable meter를 재현한 것은 아니다. 완료되지 않은 인스턴스, 누락된 이벤트, 조회 기간, retention과 Adaptive dataset의 특성이 남아 있어 비용 경보용 근사치로만 취급해야 한다.

조회는 account 범위와 lifecycle 원문을 보존한다

Workflows Analytics는 account-scoped dataset이다. 공식 raw 예시를 경보 관찰에 맞추면 viewer.accounts 아래 workflowsAdaptive에서 시간, workflow, instance, event type과 step 순번을 함께 가져오는 형태가 된다.

query WorkflowSteps($accountTag: string!, $start: Time, $end: Time) {
  viewer {
    accounts(filter: { accountTag: $accountTag }) {
      workflowsAdaptive(
        limit: 100
        filter: { datetime_geq: $start, datetime_leq: $end }
        orderBy: [datetime_ASC]
      ) {
        datetime
        workflowName
        instanceId
        eventType
        stepCount
      }
    }
  }
}

실제 요청 endpoint는 https://api.cloudflare.com/client/v4/graphql이며 token에는 대상 account의 Account Analytics: Read 권한을 최소 범위로 부여한다. token과 account ID는 환경변수로 주입하고 출력하지 않는다. 위 limit: 100은 구조를 보여주는 예시일 뿐 월 전체를 한 번에 조회한다는 뜻이 아니다. node별 기간과 record 제한은 account·plan·dataset 설정에서 확인해야 한다.

집계기는 다음 정보를 결과와 함께 남겨야 한다.

  1. account billing period의 시작과 끝
  2. 실제 query window와 반환 행 제한
  3. 마지막 query 성공 시각과 마지막 event 시각
  4. instanceId, stepName, eventType별 drill-down 근거

공개 pricing은 Paid 포함량을 월 단위로 설명하지만, 이 조사에서는 reset timestamp를 확인하지 못했다. 임의로 UTC 월초를 사용하면 기간 경계부터 틀릴 수 있다. Workflows metrics의 보존 기간도 31일이므로 billing period와 조회 기간이 어긋나거나 경계가 잘리는지 확인해야 한다.

50·80·100%는 공식 정책이 아니라 운영 상태다

공식 Workflows pricing에 따르면 Workers Paid에는 월 500,000 steps가 포함되고, 초과분은 100,000 steps당 0.80달러다. 아래 fixture는 이 공식 포함량과 단가에 HuntLab의 운영 임계값을 적용했다.

node -e '
const included = 500000;
const ratePer100k = 0.80;
const scenarios = [
  { scenario: "normal", instances: [100000, 150000] },
  { scenario: "warning", instances: [175000, 225000] },
  { scenario: "stop", instances: [200000, 300000] },
  { scenario: "over", instances: [250000, 300000] }
];
const classify = (steps) => steps >= included ? "STOP" : steps >= included * 0.8 ? "WARNING" : steps >= included * 0.5 ? "WATCH" : "OK";
for (const { scenario, instances } of scenarios) {
  const usedSteps = instances.reduce((sum, value) => sum + value, 0);
  console.log(JSON.stringify({ scenario, usedSteps, budgetPct: usedSteps / included * 100, status: classify(usedSteps), estimatedStepOverageUsd: Math.max(0, usedSteps - included) / 100000 * ratePer100k }));
}
'; printf 'exit_code=%s\n' "$?"

{"scenario":"normal","usedSteps":250000,"budgetPct":50,"status":"WATCH","estimatedStepOverageUsd":0}
{"scenario":"warning","usedSteps":400000,"budgetPct":80,"status":"WARNING","estimatedStepOverageUsd":0}
{"scenario":"stop","usedSteps":500000,"budgetPct":100,"status":"STOP","estimatedStepOverageUsd":0}
{"scenario":"over","usedSteps":550000,"budgetPct":110.00000000000001,"status":"STOP","estimatedStepOverageUsd":0.4}
exit_code=0

50·80·100·110% 상태와 550,000 steps 예상 초과비를 계산한 명령과 출력

110.00000000000001은 JavaScript number의 부동소수점 표현이므로 화면에서는 반올림한다. 550,000 steps의 0.40달러는 step 초과분만 계산한 예상값이다. Workflows 비용에는 requests/invocations, CPU time, storage도 별도 차원으로 존재하므로 이를 전체 Workers 청구액이라고 표시하면 안 된다.

상태 제안 조건 운영 행동
OK 50% 미만 추세만 기록한다.
WATCH 50% 이상 80% 미만 workflow와 instance별 증가 기울기를 확인한다.
WARNING 80% 이상 100% 미만 담당자에게 알리고 저우선순위 trigger, 설계상 step 수와 invoice를 대조한다.
STOP 검토 100% 이상 또는 기간 말 예상 초과 신규 저우선순위 실행 제한을 검토하되 필수 workflow는 예외로 둔다.
UNKNOWN/STALE query 오류 또는 freshness 초과 사용량 0으로 바꾸지 않고 관측 작업을 복구한다.

50%, 80%, 100%는 Cloudflare가 정한 경보선이 아니다. 특히 STOP은 Cloudflare의 자동 중단 상태가 아니라 운영자가 제한 여부를 검토하기 위한 내부 이름이다. 먼저 알림만 운용하면서 한 billing period 이상 GraphQL 근사치와 invoice 차이를 측정하는 편이 안전하다.

정적 설계만으로 경보를 대신해서도 안 된다. 같은 포함량과 분류 함수로 아래 전후 비교를 실행했다.

node -e '
const included = 500000;
const classify = (steps) => steps >= included ? "STOP" : steps >= included * 0.8 ? "WARNING" : steps >= included * 0.5 ? "WATCH" : "OK";
const staticSteps = 10000 * 15;
const observedSteps = 400000;
console.log(JSON.stringify({ method: "static", steps: staticSteps, budgetPct: staticSteps / included * 100, status: classify(staticSteps) }));
console.log(JSON.stringify({ method: "stepCount_fixture", steps: observedSteps, budgetPct: observedSteps / included * 100, status: classify(observedSteps) }));
console.log(JSON.stringify({ deltaSteps: observedSteps - staticSteps, decision: "use analytics signal for alert; confirm invoice separately" }));
'; printf 'exit_code=%s\n' "$?"

{"method":"static","steps":150000,"budgetPct":30,"status":"OK"}
{"method":"stepCount_fixture","steps":400000,"budgetPct":80,"status":"WARNING"}
{"deltaSteps":250000,"decision":"use analytics signal for alert; confirm invoice separately"}
exit_code=0

10,000 instances × 15 steps라는 정적 예상은 150,000 steps, 즉 30%였지만 관찰 fixture는 400,000 steps로 80%였다. 설계상 예상은 배포 전 예산에 쓰고, 실행 후 경보는 Analytics 신호로 보정하되 마지막 청구 판정은 invoice에 남겨야 한다.

retry 급증은 비용 경보와 분리한다

현재 공식 pricing은 retries와 rollback handlers를 step count에 포함하지 않는다고 명시한다. 따라서 ATTEMPT_START, ATTEMPT_FAILURE 같은 행이 급증해도 그 수를 billable steps에 더하지 않는다. 공식 step context 문서에서 WorkflowStepContext.step.count는 같은 이름의 step.do가 현재 run에서 몇 번째 호출됐는지를, attempt는 첫 실행과 재시도 횟수를 각각 나타낸다.

그렇다고 retry를 버릴 신호는 아니다. 비용 계산기와 별도의 health alert에서 ATTEMPT_*ROLLBACK_* 변화를 세고, instanceIdstepName으로 실패가 몰린 위치를 찾는다. 이 분리를 하지 않으면 장애 신호를 비용으로 오해해 실행을 막거나, 반대로 비용에 포함되지 않는다는 이유로 실제 장애를 놓칠 수 있다.

instance당 step limit도 월 포함량과 다르다. Paid의 기본 제한은 한 instance당 10,000 steps이고 설정으로 최대 25,000까지 늘릴 수 있지만, 이는 계정 월 500,000 steps 경보를 대신하지 않는다.

0과 API 오류를 정상 사용량으로 바꾸지 않는다

조회값이 비었을 때는 사용량부터 추정하지 말고 HTTP status와 GraphQL errors를 확인한다.

  • 401: token 누락, 만료 또는 잘못된 token을 먼저 확인한다.
  • 403: 대상 account resource와 Account Analytics: Read 권한을 확인한다.
  • 400: 기간, field, limit과 schema가 현재 dataset에 맞는지 확인한다.
  • 429: query 복잡도와 account/node 수를 줄이거나 quota window 뒤 재시도한다.
  • 503: 제한된 backoff로 재시도하고 지속되면 Cloudflare status page를 확인한다.

기본 user quota는 5분에 300 GraphQL queries다. Cron 주기를 짧게 만드는 것만으로 신선도가 보장되지는 않으며 resource limit에 닿을 수 있다. 성공한 응답이라도 accountTag, query window, 31일 retention과 마지막 event 시각을 함께 봐야 한다.

실패 응답이나 partial data를 0으로 저장하는 방식이 가장 위험하다. 경보 작업이 멈춘 순간 그래프가 정상으로 보이기 때문이다. 마지막 성공값에는 성공 시각을 붙이고 허용 freshness를 넘으면 STALE, 데이터 판정 자체가 불가능하면 UNKNOWN으로 전환한다. Workflows의 구체적인 ingestion delay SLA는 공개 문서에서 확인되지 않았으므로 임의의 지연 보장값을 만들지 않는다.

workflowsAdaptive라는 이름도 주의가 필요하다. 일반 GraphQL 문서는 Adaptive suffix를 adaptive sampling 표식으로 설명하지만 Workflows 문서는 이 dataset의 구체 sample rate와 오차를 밝히지 않는다. 실제 account의 introspection과 settings를 확인하기 전에는 정확한 오차 범위를 경보 로직에 넣을 수 없다.

자동 제한은 invoice 대조와 복원 경로 뒤에 둔다

Cloudflare GraphQL Analytics 안내는 해당 dataset을 billing usage의 척도로 사용하지 말라고 명시한다. 따라서 경보 결과에는 예상 또는 관찰값이라는 성격을 남기고, 대시보드와 invoice에서 실제 청구량과 적용 시작 시점을 확인해야 한다. Enterprise 계정이라면 공개 단가 대신 계약 조건도 확인한다.

100% 상태에서 자동으로 신규 실행을 제한하려면 적어도 다음 조건이 필요하다.

  • 필수·복구·보상 workflow allowlist
  • 사람이 즉시 풀 수 있는 manual override
  • query가 UNKNOWN/STALE일 때 적용할 fail-safe
  • 제한 전후의 trigger 상태와 되돌리기 절차
  • GraphQL 근사치와 invoice 차이에 대한 한 billing period 이상의 기록

이 조건이 없다면 자동 차단은 비용 오차보다 더 큰 업무 중단을 만들 수 있다. 지금 채택할 안전한 선은 80% 알림과 instance별 drill-down까지다. 100%에서는 저우선순위 trigger 제한을 검토하되, 인증된 계정에서 billing period, pagination, ingestion delay와 invoice 차이를 확인하기 전에는 자동 적용을 보류한다.

네 과금 차원의 관계를 먼저 확인하려면 기존 Cloudflare Workflows 가격 글을 읽고, 이어서 공식 Workflows pricing과 실제 invoice를 대조한다.

참고 링크

비슷한 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다