◉CLAUDE LABJP
●2.1.283 — No Claude Code release over the weekend; 2.1.283 from September 25 is still the latest. Its new auto-mode default deserves a check against your allow rules●PLANS — Pro and Team Standard now default to Opus instead of Sonnet, a change shipped in Claude Code 2.1.280 that shifts how you compare plans●10/07 — Nine days left until the old spellings of the Claude Desktop / Cowork managed config keys stop being accepted; after that they fail closed●AUTO — Users are asking why commands covered by allow rules get rejected in auto mode. The fix is shaping the invoked command to match the rule●NEW — The week Opus 5.5 became the default: three places an alias was still pointing at the old model●CURSOR — Cursor credits burn in proportion to model cost, so routing Claude through Cursor gets expensive fast. Above ~50 requests a day, flat-rate Claude Code Pro is the steadier choice●2.1.283 — No Claude Code release over the weekend; 2.1.283 from September 25 is still the latest. Its new auto-mode default deserves a check against your allow rules●PLANS — Pro and Team Standard now default to Opus instead of Sonnet, a change shipped in Claude Code 2.1.280 that shifts how you compare plans●10/07 — Nine days left until the old spellings of the Claude Desktop / Cowork managed config keys stop being accepted; after that they fail closed●AUTO — Users are asking why commands covered by allow rules get rejected in auto mode. The fix is shaping the invoked command to match the rule●NEW — The week Opus 5.5 became the default: three places an alias was still pointing at the old model●CURSOR — Cursor credits burn in proportion to model cost, so routing Claude through Cursor gets expensive fast. Above ~50 requests a day, flat-rate Claude Code Pro is the steadier choice
Articles/Claude Code
⟐ Claude Code/2026-06-14Intermediate

Measuring a Week of Headless Usage the Night Before the Billing Change

With headless Claude Code moving to monthly credits on June 15, I spent a week logging how many tokens my unattended runs actually consume, so I could pick a plan based on numbers instead of a guess.

Claude Code257headless14cost optimization13indie dev10operations30

Starting tomorrow (June 15), headless claude -p, the Claude Agent SDK, and GitHub Actions runs move to a separate pool of monthly credits, no longer drawn from your subscription limits. The thing that tripped me up was simple: I had nothing on hand to decide whether the new plan would be enough. Would Pro's monthly credits cover me, or did I need Max? I genuinely couldn't tell by feel. As an indie developer juggling several unattended jobs, I wanted that answer in numbers, not vibes. A single day isn't enough for a precise forecast, but I at least wanted a number for roughly how many tokens my unattended runs burn. So I spent this past week logging it.

The short version: only after measuring did I learn that my nightly batches were heavier than I assumed, while the one-off daytime runs were basically rounding error. Picking a plan is far easier once you can see that breakdown.

Why a single "total tokens" number misleads you

When billing comes up, it's tempting to look only at the grand total for the month. But once you move to monthly credits, what matters is less the total and more which jobs are eating the credits. Two months with the same total behave completely differently if one is "a few heavy runs" and the other is "many light runs" — the place you'd cut is not the same.

Headless runs in particular hide cost in output tokens and in prompt-cache reads and writes. Claude's usage object separates input_tokens / output_tokens from cache_creation_input_tokens and cache_read_input_tokens, and there's an asymmetry: cache reads are cheap, cache creation is pricey. If you only watch the total, you miss that structure entirely, so I decided to record everything split by job and by token type.

Wrap each run and append usage one line at a time

What I did is almost embarrassingly plain: wrap each headless run in a thin shim and append one JSONL line of usage when it finishes. I swapped the Agent SDK jobs over to call through this wrapper.

// usage-logger.mjs
import { appendFileSync } from "node:fs";
import { query } from "@anthropic-ai/claude-agent-sdk";
 
const LOG_PATH = process.env.USAGE_LOG ?? "./headless-usage.jsonl";
 
// job: a label for what this run is ("nightly-build", etc.)
export async function runWithUsage(job, prompt, options = {}) {
  const startedAt = new Date().toISOString();
  let usage = null;
 
  for await (const message of query({ prompt, options })) {
    // The SDK emits a final result message that carries usage
    if (message.type === "result" && message.usage) {
      usage = message.usage;
    }
  }
 
  if (usage) {
    const row = {
      job,
      startedAt,
      finishedAt: new Date().toISOString(),
      input: usage.input_tokens ?? 0,
      output: usage.output_tokens ?? 0,
      cacheCreate: usage.cache_creation_input_tokens ?? 0,
      cacheRead: usage.cache_read_input_tokens ?? 0,
    };
    appendFileSync(LOG_PATH, JSON.stringify(row) + "\n");
  }
  return usage;
}

Two things matter here. First, always attach a job label — without it you can't split the breakdown later. I just used my real job names (nightly build, integrity check, article generation). Second, capture the usage from the result message. Usage-like figures stream by mid-flight, but the settled value lives in the final result. Summing the intermediate values double-counts, so record only the last one.

I use JSONL (one record per line) because appending is safe and hard to corrupt. Even if several cron jobs write at once, line-oriented writes rarely interleave, and the aggregation step can read it line by line. A CSV that loses a column mid-file becomes unreadable; with JSONL you drop the one broken line and the rest survives.

Fold a week down by job

After a week, sum by job and estimate the cost. Pricing is per MTok, so you divide token counts by a million and multiply (the rates below are a rough example using API input $10 / output $50; your actual charge follows your plan's credit conversion).

// summarize-usage.mjs
import { readFileSync } from "node:fs";
 
const PRICE = { input: 10, output: 50, cacheCreate: 12.5, cacheRead: 1 }; // $/MTok (example)
 
const rows = readFileSync(process.env.USAGE_LOG ?? "./headless-usage.jsonl", "utf8")
  .split("\n")
  .filter(Boolean)
  .map((line) => JSON.parse(line));
 
const byJob = {};
for (const r of rows) {
  const j = (byJob[r.job] ??= { runs: 0, input: 0, output: 0, cacheCreate: 0, cacheRead: 0 });
  j.runs += 1;
  j.input += r.input;
  j.output += r.output;
  j.cacheCreate += r.cacheCreate;
  j.cacheRead += r.cacheRead;
}
 
const cost = (t) =>
  (t.input * PRICE.input +
    t.output * PRICE.output +
    t.cacheCreate * PRICE.cacheCreate +
    t.cacheRead * PRICE.cacheRead) /
  1_000_000;
 
const table = Object.entries(byJob)
  .map(([job, t]) => ({ job, runs: t.runs, weekUSD: +cost(t).toFixed(2), monthUSD: +(cost(t) * 30 / 7).toFixed(2) }))
  .sort((a, b) => b.monthUSD - a.monthUSD);
 
console.table(table);
console.log("Estimated monthly total $", table.reduce((s, r) => s + r.monthUSD, 0).toFixed(2));

monthUSD is just "the week's actuals scaled by 30/7." It isn't rigorous, but for lining up against a plan limit to judge "fits / doesn't fit," it was plenty. Sending the output to console.table lays every job out in a row, so the order in which you'd trim them is right there.

What only showed up once I measured

In my case, the heavy batch that runs overnight accounted for most of the monthly figure, while the daytime one-offs were rounding error even summed together. I'd assumed the frequent daytime runs were the cost driver, so this was the exact opposite of my hunch. It's the weight per run, not the frequency, that dominates — obvious in hindsight, but seeing it in my own numbers was the payoff.

The other surprise was that for one job the cacheRead token count was several times the input. That job feeds the same large preamble (repo conventions, a template) every run, and the cache kept it cheap. Which is also a warning: rewrite it in a way that defeats the cache and this column spikes. If you're going to touch prompt structure during the migration, keep one eye on this number.

What I decided with the time left

A week isn't much, but once the breakdown was in numbers the decision was anticlimactic. I trimmed the heavy nightly batch down to two jobs, left the frequent light runs alone for now to stay inside the monthly-credit envelope, and paired it with a three-tier Claude Code fallbackModel so an overloaded morning doesn't stall the run. The plan mechanics themselves are in how the June 15 Claude Code billing change affects headless runs, and the method for reading ahead from early-month data is in forecasting Claude API token cost from the first three days.

There isn't much you can do the night before a billing change, but try dropping usage-logger.mjs over your runs and capturing even a single batch tonight. One logged line turns the plan conversation from "probably fine" into "this job costs $X a month." Thanks for reading.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-06-17
When an Announced Billing Change Gets Paused at the Last Minute: Designing Automation That Doesn't Rush the Cutover
A billing change that was supposed to take effect on June 15 was paused that same day. If your pipeline trusts the announced date, a retraction breaks it twice. Here is a design that decides the cutover from a runtime signal, with implementation code.
⟐ Claude Code2026-09-22
Taking Inventory of Worktrees That Were Supposed to Be Gone
Isolated worktrees my subagents created were never folded away. Here is what I measured in git 2.34.1 about when remove succeeds, the branches that outlive the trees, the 12 MB per-tree difference, and how I decide who owns cleanup.
⟐ Claude Code2026-09-01
Once I passed a dozen MCP servers, I stopped trusting the startup list
Only some of the servers that say connection failed at startup are ones you can actually fix. Here is a probe that separates no-response, launch failure, and protocol rejection, measured across a 14-server fleet where sequential 22.03s became parallel 5.09s, and where trimming the deadline quietly turned healthy servers into failures.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links