Concepts
Usage and limits
What Temper meters for each environment, how to read it, and how monthly limits suspend environments that go over.
Temper meters what each environment uses, hour by hour. You can read usage for any time range, per environment and in total, and set monthly limits so that a runaway agent or an unexpectedly busy end user cannot use more than you planned for.
Metrics
| Metric | What it measures |
|---|---|
active_hours | Time the environment's VM was running. Time while it is paused, suspended or stopped does not count. |
cpu_hours | CPU time used by the VM. |
memory_gib_hours | Memory held by the VM over time, in GiB-hours. |
storage_gib_hours | Data disk space used over time, in GiB-hours. |
egress_gib | Network traffic sent out of the environment, in GiB. |
browser_hours | Time spent in browser leases that use a profile (from lease start to end). Clean leases do not count. |
Hosts report cumulative counters and the control plane records the increase in each hour. Time while a host is out of contact is not counted.
Reading usage
Get usage returns the totals for a time range and one entry per environment.
| Parameter | Meaning |
|---|---|
from | Start of the range (inclusive). Defaults to the start of the current month, 00:00 UTC. |
to | End of the range (exclusive). Defaults to now. |
environment_id | Only this environment. Returns not_found if it is not yours. |
from must be earlier than to, otherwise the request fails with bad_request. The response has from, to, total (all six
metrics summed) and data: one entry per environment with environment_id, end_user_id and the six metrics. Environments with no
usage in the range are left out of data.
import { Temper } from "@temper-hq/sdk";
const temper = new Temper();
const usage = await temper.metering.usage(); // this month (UTC) so far
console.log("total egress GiB:", usage.total.egress_gib);
for (const e of usage.data) {
console.log(e.end_user_id, e.active_hours, e.browser_hours);
}
// One environment, a fixed range.
const sept = await temper.metering.usage({
from: "2026-09-01T00:00:00Z",
to: "2026-10-01T00:00:00Z",
environment_id: envId,
});from datetime import datetime, timezone
from temper_hq import Temper
temper = Temper()
usage = temper.metering.usage() # this month (UTC) so far
print("total egress GiB:", usage.total.egress_gib)
for e in usage.data:
print(e.end_user_id, e.usage.active_hours, e.usage.browser_hours)
# One environment, a fixed range.
sept = temper.metering.usage(
from_=datetime(2026, 9, 1, tzinfo=timezone.utc),
to=datetime(2026, 10, 1, tzinfo=timezone.utc),
environment_id=env_id,
)usage, err := client.Metering.Usage(ctx, temper.UsageParams{}) // this month (UTC) so far
if err != nil {
log.Fatal(err)
}
fmt.Println("total egress GiB:", usage.Total.EgressGiB)
for _, e := range usage.Data {
fmt.Println(e.EndUserID, e.ActiveHours, e.BrowserHours)
}
// One environment, a fixed range.
sept, err := client.Metering.Usage(ctx, temper.UsageParams{
From: time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC),
To: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC),
EnvironmentID: envID,
})Monthly limits
A limit caps one metric for the current calendar month (UTC). There are two kinds:
- Developer-level: no
environment_id. The cap applies to the sum across all your environments. - Per environment: with an
environment_id. The cap applies to that environment alone.
Setting a limit for a metric that already has one (at the same level) replaces it. monthly_limit must be
zero or more; an unknown metric or a negative limit returns bad_request. Listing limits returns every
limit, developer-level ones first, each with used for the current month.
// At most 50 GiB of egress per month across all environments.
await temper.metering.setLimit({ metric: "egress_gib", monthly_limit: 50 });
// At most 20 browser hours per month for one environment.
await temper.metering.setLimit({ metric: "browser_hours", monthly_limit: 20, environment_id: envId });
for (const l of await temper.metering.listLimits()) {
console.log(l.environment_id ?? "all environments", l.metric, `${l.used} / ${l.monthly_limit}`);
}# At most 50 GiB of egress per month across all environments.
temper.metering.set_limit(metric="egress_gib", monthly_limit=50)
# At most 20 browser hours per month for one environment.
temper.metering.set_limit(metric="browser_hours", monthly_limit=20, environment_id=env_id)
for lim in temper.metering.list_limits():
print(lim.environment_id or "all environments", lim.metric, f"{lim.used} / {lim.monthly_limit}")// At most 50 GiB of egress per month across all environments.
if _, err := client.Metering.SetLimit(ctx, temper.SetLimitParams{Metric: temper.MetricEgressGiB, MonthlyLimit: 50}); err != nil {
log.Fatal(err)
}
// At most 20 browser hours per month for one environment.
if _, err := client.Metering.SetLimit(ctx, temper.SetLimitParams{
Metric: temper.MetricBrowserHours, MonthlyLimit: 20, EnvironmentID: envID,
}); err != nil {
log.Fatal(err)
}
limits, err := client.Metering.ListLimits(ctx)
if err != nil {
log.Fatal(err)
}
for _, l := range limits {
fmt.Println(l.EnvironmentID, l.Metric, l.Used, l.MonthlyLimit)
}When a limit is exceeded
A limit counts as exceeded when usage for the month is greater than or equal to the limit, so a limit of 0 allows nothing.
Limits are checked every minute, and again right away whenever you set or delete one. When a limit is exceeded:
- The environment is suspended. If a developer-level limit is exceeded, every environment that is not destroyed is suspended.
- The environment's
over_limitfield is set to a human-readable reason, such asenvironment egress_gib 1.20 >= 1. It isnullwhen the environment is within its limits. - A
limit_exceededenvironment event is recorded and anenvironment.limit_exceededwebhook is sent. - Resume, wake and exec on the environment fail with
409 limit_exceeded.
This lasts until you raise the limit, delete it, or a new month starts. At that point over_limit goes back to null, but the
environment stays suspended until you resume it. Suspending keeps the data disk; see
environments.
import { Temper, isTemperError } from "@temper-hq/sdk";
try {
await temper.environments.resume(envId);
} catch (err) {
if (isTemperError(err, "limit_exceeded")) {
const env = await temper.environments.get(envId);
console.log("over limit:", env.over_limit);
} else {
throw err;
}
}from temper_hq import TemperError
try:
temper.environments.resume(env_id)
except TemperError as err:
if err.code != "limit_exceeded":
raise
print("over limit:", temper.environments.get(env_id).over_limit)if _, err := client.Environments.Resume(ctx, envID); err != nil {
if !temper.IsCode(err, "limit_exceeded") {
log.Fatal(err)
}
env, err := client.Environments.Get(ctx, envID)
if err != nil {
log.Fatal(err)
}
fmt.Println("over limit:", *env.OverLimit)
}Deleting a limit
Delete a limit by metric, plus environment_id for a per-environment limit (leave it out to delete the
developer-level one). Deleting a limit that does not exist returns not_found. Limits are rechecked right away, so environments that
were suspended only because of that limit can be resumed.
await temper.metering.deleteLimit({ metric: "egress_gib" }); // developer-level
await temper.metering.deleteLimit({ metric: "browser_hours", environment_id: envId });temper.metering.delete_limit(metric="egress_gib") # developer-level
temper.metering.delete_limit(metric="browser_hours", environment_id=env_id)if err := client.Metering.DeleteLimit(ctx, temper.MetricEgressGiB, ""); err != nil { // developer-level
log.Fatal(err)
}
if err := client.Metering.DeleteLimit(ctx, temper.MetricBrowserHours, envID); err != nil {
log.Fatal(err)
}Related
- Usage API reference
- Environments and the idle ladder, which decides how much active time an environment uses