Two clocks, and why conflating them costs money
- A batch discount is priced on a deadline. You surrender latency, accept a completion window of up to 24 hours, and the provider chooses when the work runs. You do not.
- A time-of-day tier is priced on the wall clock. The rate depends on the hour of dispatch, you pick that hour, and latency does not change at all.
- They compose; they are not alternatives. One trades responsiveness, the other scheduling freedom. A job may be eligible for both, for one, or for neither.
- Deadline is the classifier, not importance. The nightly reconciliation finance depends on is important and completely deferrable. The autocomplete nobody remembers is trivial and not deferrable at all.
- The ceiling is arithmetic, not effort. Where off-peak is half of peak, this lever returns at most half of the spend you can actually move — so measure the movable fraction before building anything.
Most teams learned one cost lever early and stopped there. The batch lever is a trade of latency for money, and our companion guide to the batch API and the 50% async discount covers its mechanics in full, so this article will not re-teach them: OpenAI's Batch API takes a flat 50% off every model in exchange for a 24-hour completion window, and Anthropic's Message Batches API does the same, with cache reads at 0.1 times base input — Sonnet 4.6 at $3 and $15 per million tokens becomes $1.50 and $7.50 batched. You hand over control of when, and you get a price for it.
The wall-clock lever asks for something else. You keep the synchronous endpoint, the same latency and the same response shape; you simply do not press send between certain hours. Nothing is queued at the provider, and a request that returns in 900 milliseconds at peak returns in 900 milliseconds off-peak. What you give up is the freedom to run whenever you feel like it — which, for a great deal of production work, costs nothing, because that work already runs on a timer somebody chose arbitrarily.
Conflating the two is expensive in both directions. Teams that assume "we already batch, so we are done" leave the wall-clock discount untouched on every synchronous request; teams that hear "off-peak is half price" and route latency-sensitive traffic into a holding queue break a product to save pennies. The rest of this guide separates them and builds the scheduler that exploits the second.
What DeepSeek actually changed in August 2026
The trigger is concrete. DeepSeek announced peak and off-peak API pricing around 14 August 2026, with the new rates taking effect at 16:00 UTC on 16 August 2026 — midnight on 17 August in Beijing, where the pricing clock is set. Peak hours are 09:00 to 12:00 and 14:00 to 18:00 Beijing time (UTC+8); every other hour is off-peak, which means seven peak hours and seventeen off-peak hours on a 24-hour cycle. The ratio is the cleanest part of the design: off-peak is exactly half of peak on every line item — cache-hit input, cache-miss input and output alike.
| Model and rate line (RMB per million tokens) | Peak | Off-peak |
|---|---|---|
| DeepSeek-V4-Flash — cache-hit input | 0.10 | 0.05 |
| DeepSeek-V4-Flash — cache-miss input | 3 | 1.5 |
| DeepSeek-V4-Flash — output | 9 | 4.5 |
| DeepSeek-V4-Pro — cache-hit input | 0.30 | 0.15 |
| DeepSeek-V4-Pro — cache-miss input | 9 | 4.5 |
| DeepSeek-V4-Pro — output | 27 | 13.5 |
Rates as published for the tiering that took effect 16:00 UTC on 16 August 2026. Confirm current figures against the provider's pricing page before designing around them.
Because every line halves by the same factor, the composition of your traffic is irrelevant to the saving. A V4-Pro job consuming 400 million cache-miss input tokens and producing 40 million output tokens costs 400 × 9 + 40 × 27 = RMB 4,680 at peak, and 400 × 4.5 + 40 × 13.5 = RMB 2,340 off-peak. No token-mix model is needed. What you do need is an honest estimate of the fraction of spend that can move, because the saving on the whole bill is that fraction divided by two. Move 40% off-peak and the total falls by 20%; move 70% and it falls by 35%. Fifty per cent is the unreachable ceiling, available only if everything defers.
Do not read this as free money. Measured against DeepSeek's previous rates, the August 2026 restructuring was a price increase for some workloads rather than a discount, and a fair amount of the coverage highlighted exactly that. Off-peak is half of the new peak, not half of what you were paying in July. Recovering part of a rise by scheduling is worth doing — and worth describing accurately in your own cost review.
The context is worth noting without over-reading it. Nvidia raised AI hardware prices by more than 15% in August 2026, and AWS raised EC2 Capacity Blocks rates by roughly 20%, about six months after a 15% rise in January 2026 — while enterprise inference prices reached a 2026 low as the API price war intensified. Capacity is getting dearer while a token gets cheaper, and part of how that gap closes is by making capacity a scheduling problem for the customer. Nobody can promise other providers will copy the structure, so keep the windows configurable rather than hard-coding one vendor's hours.
Classify by deadline, not by importance
Whether any of this works comes down to how you sort your traffic, and the criterion is counter-intuitive. Not importance, not cost, not job size, but: how long can this wait before somebody notices? Four classes cover nearly everything, and each behaves differently in a scheduler.
| Class | Slack available | Typical examples | Scheduler behaviour |
|---|---|---|---|
| Interactive | None — a person is waiting | Chat turns, autocomplete, an agent step a user is watching, a synchronous webhook a partner times out on | Never deferred, never queued, dispatched immediately at whatever rate applies |
| Soft deadline | Hours | A report requested before end of day, moderation of a queue reviewed each afternoon, enrichment of records a colleague will read tomorrow morning | Deferred to the next off-peak window if it comfortably clears the deadline; released early if the backlog threatens it |
| Background | Overnight, or a full day | Nightly catalogue enrichment, daily analytics summarisation, index rebuilds, scheduled digest generation | Pinned to off-peak by default; the deadline is a safety net that rarely fires |
| Rebuildable | Effectively unbounded | Embeddings backfills, eval-suite runs, summarisation of yesterday's logs, re-scoring a corpus after a prompt change | Fully elastic: run only off-peak, pause freely, resume later, and drop mid-flight without consequence |
The fourth class deserves attention: it is where most of the recoverable spend hides, and it is the class teams most often misfile. Rebuildable work can be re-derived at any time from inputs you still hold. An embeddings backfill is rebuildable — interrupted at 60%, nothing is lost but the compute. So is an eval-suite run over a fixed test set, and so is summarising yesterday's logs, because yesterday's logs are still there. This is the same property that makes a job safe on interruptible hardware, and if you have worked through the spot and pre-emptible GPU checkpoint-and-resume playbook, you already have the discipline this class needs.
Two sorting mistakes recur. The first is confusing importance with urgency: a finance reconciliation job is business-critical and entirely deferrable, while a cosmetic UI suggestion is trivial and entirely non-deferrable. The second is confusing "runs on a schedule" with "must run at that time". A job firing at 14:00 because somebody wrote that in a cron file eight months ago is bound to 14:00 by an accident, and accidents are the cheapest thing in the system to change. Auditing the crontab for hours nobody chose deliberately is often the highest-return hour in the exercise.
Make cost class a required field where work is created, not a column somebody backfills later. If a caller must state a class and a deadline to enqueue anything, the classification stays honest. If the field is optional, every task quietly defaults to interactive, and within a quarter your scheduler is a queue that never defers anything.
The deferrable queue: a design that will not starve
The mechanism is a queue with an admission decision. Every task carries what the dispatcher needs to answer one question — send now, or hold until the tier turns over? — and the record schema is where the design lives.
# scheduler/records.py — the unit of deferrable work.
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any
class CostClass(str, Enum):
"""How much freedom the scheduler has with this task."""
INTERACTIVE = "interactive" # a person is waiting: never defer
SOFT = "soft" # hours of slack
BACKGROUND = "background" # overnight is fine
REBUILDABLE = "rebuildable" # re-derivable at any time; fully elastic
@dataclass
class Task:
task_id: str # stable, caller-supplied
idempotency_key: str # SAME key on every attempt of this task
cost_class: CostClass
payload: dict[str, Any]
created_at: datetime # aware UTC
deadline: datetime # hard: past this, run regardless of price
not_before: datetime # earliest the dispatcher may send it
attempts: int = 0
base_priority: int = 0 # higher runs first, before ageing
tier_at_dispatch: str | None = None # "peak" | "off_peak", stamped on send
est_runtime_s: float = 30.0 # used to project backlog drain
def effective_priority(self, now: datetime) -> float:
"""Ageing, so a cheap tier can never starve old work.
Two terms: time already waited, and urgency that rises sharply as
the deadline closes. Both are monotonic in `now`, so ordering is
stable between polls and nothing oscillates.
"""
waited_h = (now - self.created_at).total_seconds() / 3600.0
slack_h = max((self.deadline - now).total_seconds() / 3600.0, 0.0)
urgency = 1.0 / (1.0 + slack_h) # -> 1.0 as slack -> 0
return self.base_priority + 0.5 * waited_h + 100.0 * urgency
Five fields carry the design. not_before is the release valve: set it to the next off-peak instant and the dispatcher will not look at the task until then. deadline stops thrift becoming failure — past it, the scheduler pays peak rates without hesitating. cost_class decides whether deferral is permitted at all. idempotency_key makes a retry across a tier boundary safe, and must be stable across attempts, which is why it is supplied rather than generated. And tier_at_dispatch is stamped on every send, because without it you cannot prove the scheduler did anything.
Ageing is the part teams leave out and regret. Without it, a queue ordered purely by priority keeps a low-priority backfill waiting behind every newer arrival indefinitely, and the first anyone hears of it is a stale index. The dispatcher itself is short, and every subtlety in it is about time zones or deadlines.
# scheduler/dispatch.py — tier-aware admission control.
from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo
from .records import CostClass, Task
# The provider states its windows in Beijing time. Evaluate in the PROVIDER's
# zone, never in the server's local zone and never in a hard-coded offset.
PROVIDER_TZ = ZoneInfo("Asia/Shanghai")
PEAK_WINDOWS = ((time(9, 0), time(12, 0)), (time(14, 0), time(18, 0)))
DEFERRABLE = {CostClass.SOFT, CostClass.BACKGROUND, CostClass.REBUILDABLE}
def tier_at(moment: datetime) -> str:
"""'peak' or 'off_peak' for an instant, in the provider's own timezone."""
if moment.tzinfo is None:
raise ValueError("naive datetime: always pass an aware UTC instant")
local = moment.astimezone(PROVIDER_TZ).time()
for start, end in PEAK_WINDOWS:
if start <= local < end:
return "peak"
return "off_peak"
def next_off_peak(moment: datetime) -> datetime:
"""First instant at or after `moment` that is off-peak.
A minute-by-minute walk: slower than closed-form arithmetic, but it
stays correct if the windows change shape, and a scheduler runs this
once per poll, not once per request.
"""
probe = moment.astimezone(PROVIDER_TZ).replace(second=0, microsecond=0)
for _ in range(24 * 60):
if tier_at(probe) == "off_peak":
return probe.astimezone(timezone.utc)
probe += timedelta(minutes=1)
raise RuntimeError("no off-peak minute in 24h — check PEAK_WINDOWS")
def should_dispatch(task: Task, now: datetime, backlog_s: float) -> tuple[bool, datetime]:
"""Send now, or hold until the tier turns over?
`backlog_s` is the projected seconds of queued work ahead of this task.
Returns (send_now, not_before).
"""
if task.cost_class not in DEFERRABLE:
return True, now # rule 1: never make a human wait
if tier_at(now) == "off_peak":
return True, now # rule 2: already cheap
resume = next_off_peak(now)
# rule 3: would waiting, PLUS draining the backlog, blow the deadline?
projected_finish = resume + timedelta(seconds=backlog_s + task.est_runtime_s)
if projected_finish >= task.deadline:
return True, now # urgency outranks price
return False, resume # rule 4: hold
def on_retry(task: Task, now: datetime, backlog_s: float = 0.0) -> bool:
"""A retry is a NEW billable request, so it gets its own tier decision.
Without this, a job dispatched at RMB 4.5 off-peak retries at RMB 9
an hour later and nobody notices until the monthly invoice.
"""
task.attempts += 1
if task.tier_at_dispatch == "off_peak" and tier_at(now) == "peak":
send, resume = should_dispatch(task, now, backlog_s)
task.not_before = resume
return send
return True
Three details are worth lifting. tier_at refuses a naive datetime outright — every instant here is timezone-aware UTC, converted to the provider's zone only for the comparison. The deadline check in should_dispatch includes the backlog, not only the tier boundary: releasing a task at the start of off-peak means nothing if six hours of work sit in front of it. And on_retry exists because a retry is a fresh billable request, which a naive policy will happily re-send into peak hours at double the rate.
Idempotency deserves its own sentence. Once work can be held, released and retried across a tier boundary, a double send has to be harmless: derive the key from the input rather than the attempt, and make the downstream write an upsert on it. A scheduler that can silently double a bill is worse than no scheduler.
Peak hours belong to somebody else's working day
Here is where a Beijing-anchored pricing clock stops being an abstraction. Convert the peak windows into the zones our readers work in, and the strategy differs sharply between India and the United Kingdom.
| Window | Beijing (UTC+8) | UTC | IST (UTC+5:30) | BST (UTC+1) |
|---|---|---|---|---|
| Peak — morning | 09:00–12:00 | 01:00–04:00 | 06:30–09:30 | 02:00–05:00 |
| Peak — afternoon | 14:00–18:00 | 06:00–10:00 | 11:30–15:30 | 07:00–11:00 |
| Off-peak — short block | 12:00–14:00 | 04:00–06:00 | 09:30–11:30 | 05:00–07:00 |
| Off-peak — long block | 18:00–09:00 | 10:00–01:00 | 15:30–06:30 | 11:00–02:00 |
For a UK team, that table is close to a reprieve. A working day starting at 09:00 BST loses only its first two hours to peak pricing, and from 11:00 BST onward everything — the rest of the day, the evening, the whole overnight batch window — is already off-peak. The long off-peak block runs from 11:00 BST until 02:00 the following morning. A London or Manchester team that changes nothing is already paying off-peak for the large majority of its traffic, and the honest conclusion is that the engineering effort belongs elsewhere: routing and caching will return more than a scheduler will.
For an Indian team, the same windows are genuinely awkward. The morning peak at 06:30–09:30 IST clips the start of the day, and the afternoon peak at 11:30–15:30 IST sits precisely in the middle of it. Four hours of prime working time are billed at double — exactly when a Bengaluru or Pune team is running evaluations, re-indexing after a deploy and firing off ad-hoc analysis. The remedy is unusually cheap on freshness, though: the long off-peak block opens at 15:30 IST, so most deferrals are a wait of a few hours rather than overnight. Holding an eval run from 12:30 to 15:30 IST halves its cost and delays a result nobody was going to read before the stand-up anyway.
This is one of the clearer dual-market divergences in cost engineering: the same feature, provider and rates yield "not worth building" in the UK and "build it this quarter" in India. If your organisation spans both — a Chennai delivery team serving a London product team is a common shape — set the provider's zone once in configuration and let each team's cost report show its own exposure, rather than letting one office's indifference set policy for the other's bill.
Every article here is written by a Verified Builder. Want your name on the next one?
AI Tech Connect lists AI engineers, founders and researchers across India and the UK — and the people hiring browse it to find them. Adding your profile is free.
Become a Verified Builder →Composing the two levers
Because the levers are priced on different clocks, they apply to the same task without competing. A rebuildable embeddings backfill is both deferrable to an off-peak hour and tolerant of a 24-hour window; an interactive chat turn is neither; a report due in three hours is deferrable by a couple of hours but cannot survive a 24-hour window.
That third case is where the levers diverge. A batch endpoint gives you a completion window, not a completion time — the work may return in twenty minutes or twenty hours, and you must design for the ceiling. An off-peak dispatch returns at normal latency; you merely chose when to start. So for a same-day deadline measured in hours, the wall-clock lever is usable and the batch lever is not. For anything overnight or unbounded, both apply: hold until off-peak, then submit to the batch endpoint.
Do not assume the two discounts multiply on one request unless the provider says so. As of August 2026 the batch discounts and the peak tiering described here come from different providers, so no published rate card gives one request both. What is reliably true is that they compose across a portfolio: use the batch endpoint where a provider offers it, use the clock where a provider prices by it, and let each job take whichever discounts its own provider actually publishes.
Both levers sit downstream of two cheaper ones. Routing and cascades change which model answers at all, a larger multiplier than any scheduling decision, and prompt caching changes how many tokens you are billed for — and it interacts directly with tiering, since DeepSeek's cache-hit line halves off-peak like everything else: RMB 0.05 per million input tokens on V4-Flash against RMB 0.10 at peak. Get routing and caching right first. Scheduling is the lever you pull on the bill that remains.
Measuring it without fooling yourself
A scheduler that cannot prove its own value gets deleted in the next refactor, and rightly. The measurement that matters is not "how much did we spend" but "what did we pay per unit of work, split by tier" — which means stamping tier_at_dispatch on every request at send time, beside the token counts and the cost.
-- Effective blended rate, split by the tier each request was billed at.
-- Run daily; the interesting column is the trend in rmb_per_mtoken.
SELECT
date_trunc('day', dispatched_at) AS day,
tier_at_dispatch AS tier,
cost_class,
count(*) AS requests,
sum(input_tokens + output_tokens) / 1e6 AS mtokens,
sum(cost_rmb) AS spend_rmb,
sum(cost_rmb)
/ nullif(sum(input_tokens + output_tokens) / 1e6, 0) AS rmb_per_mtoken
FROM llm_requests
WHERE dispatched_at >= now() - interval '30 days'
GROUP BY 1, 2, 3
ORDER BY 1, 2, 3;
Three numbers come out of that. The share of spend dispatched off-peak says whether the scheduler is doing anything. The blended effective rate — total spend over total tokens — is what you actually pay on average, the only rate that reaches an invoice. And the split by cost_class says whether your classification is honest: if 90% of traffic is tagged interactive, the default is doing the tagging for you.
The trap is optimising the rate while volume grows underneath it. A blended rate that falls 30% while token consumption doubles produces a bill that rose 40%, and a dashboard showing only the rate reports a triumph. This is why the denominator must be a unit of business work rather than a token: cost per task, per document, per resolved ticket. Our guide to LLM unit economics and cost per task sets out how to build that denominator so it survives a change of model or prompt, and for a multi-tenant product per-tenant cost attribution stops one heavy customer's deferred backfill being averaged into everyone else's margin.
Track one more thing: deferral latency, the time each task sat between created_at and dispatch. That series is your early warning for a scheduler drifting from thrift into obstruction, and it is the number a product manager will ask for the first time a report arrives late.
Common pitfalls, and when not to build this at all
Four failure modes account for most of the trouble. All are cheap to prevent and expensive to discover.
- Retries that cross a tier boundary. A request dispatched at 08:55 Beijing time fails, the retry policy waits ten minutes, and every attempt from 09:00 onward pays double — silently. Retry policies written before tiered pricing have no opinion about price. Give the retry path its own tier decision, as
on_retrydoes above, and log the tier of every attempt so the invoice can be explained. - Clock and timezone bugs. Evaluating the windows in the server's local time is the classic error, and it is silent: the scheduler is confidently wrong by five and a half hours from a Mumbai host, or by seven or eight from a London one depending on the season. Evaluate in the provider's stated zone using a real timezone database — never a hard-coded offset, and never the container's local time, which may change when someone rebuilds the image.
- Deferring something a human is waiting on. The most damaging failure is not financial. A task mis-tagged as background because the default was convenient will sit in a queue while a customer refreshes a page. Enforce classification at enqueue time, and make anything originating in a request-response path interactive regardless of what the caller claims.
- A backlog that drains into peak. Seventeen off-peak hours look generous until a queue holds more work than fits in them. The queue opens at the tier boundary, drains steadily, and is still draining when peak begins — at which point the very jobs you deferred to save money are the ones paying double. Cap admission so the projected finish lands inside the window, and push the overflow to the next window rather than letting it spill.
The backlog-spill pitfall inverts your saving without any alarm firing. Instrument it directly: at each tier boundary, log queue depth and projected drain time, and alert when the projected drain exceeds the remaining off-peak minutes. A scheduler that runs into peak with a full queue has stopped optimising and started deferring work into the dearest hours of the day.
The break-even argument
Now the case against. The maximum this lever returns is half of your deferrable spend — a ceiling, not a forecast — so work that number out before writing a line of code. If deferrable spend is £400 a month, or ₹40,000, the best case is £200 or ₹20,000, set against a queue table, a dispatcher, ageing, tier-aware idempotent retries, dashboards and a permanent addition to the on-call surface. A component that can hold work is a component that can lose work.
A rule of thumb: if the ceiling does not clear a couple of weeks of a senior engineer's fully loaded cost in the first year, do not build it. Pull cheaper levers instead — routing, caching, shorter prompts, and the unglamorous discipline of not making the call at all. If it does clear that line, build the smallest version first: a not_before field on the jobs you already run on a timer, and a cron schedule shifted into the off-peak block. That is an afternoon's work, needs no new infrastructure, and often captures most of the saving, because scheduled jobs are where deferrable spend concentrates. Build the full dispatcher only once the data shows ad-hoc work is a large enough share to justify it.
The wider point survives whichever way the arithmetic falls. Inference pricing is acquiring dimensions — completion windows, cache states, and now the hour on a clock in another country — and each is a lever for teams who model their costs and a tax on teams who do not. Knowing which workloads can wait, and by how long, is becoming as ordinary a piece of production knowledge as knowing which endpoints are on the critical path.