Skip to content
Docs

Usage, limits and cancellation

This script checks what the API will accept, how much of your allowance a call uses, and what happens to that usage when you stop waiting for a call, all against the live service.
On this page
TerminalBASH
pip install -U causilo-client scikit-learn requests
python usage_limits_and_cancel.py
usage_limits_and_cancel.pyPYTHON
import os
import time

import numpy as np
import pandas as pd
import requests
from sklearn.datasets import load_diabetes

from causilo_client import (
    Causilo,
    CausiloError,
    CausiloQuotaExceeded,
    CausiloRateLimited,
    CausiloTimeout,
)

ENDPOINT = os.environ.get("CAUSILO_ENDPOINT", "https://api.nums.world")
cx = Causilo(ENDPOINT, token=os.environ["CAUSILO_KEY"])

Limits, before you send anything

GET /v1/limits needs no key. The numbers it returns are part of the API contract, so a client can check a table locally before uploading it.

usage_limits_and_cancel.pyPYTHON
limits = requests.get(f"{ENDPOINT}/v1/limits", timeout=30).json()
print("limits:", limits)


def fits(context: pd.DataFrame, query: pd.DataFrame) -> str:
    cells = (len(context) + len(query)) * (context.shape[1] - 1)
    if len(context) + len(query) > limits["max_rows"]:
        return "too many rows"
    if context.shape[1] > limits["max_cols"]:
        return "too many columns"
    if cells > limits["max_cells"]:
        return "too many cells"
    return f"fits; {cells:,} cells would be charged"


big = pd.DataFrame(np.zeros((3_000_000, 5)), columns=list("abcde"))
print("3,000,000 x 5 table:", fits(big, big.iloc[:10]))

What a call costs

A call uses one cell for each value in the tables you send, counting context and query rows together, so a 1,000 by 21 context with a 200 by 20 query uses 24,000 cells. Your account has a monthly and a daily allowance, and usage() reads both without using anything.

usage_limits_and_cancel.pyPYTHON
u = cx.usage()
print(pd.DataFrame(u["scopes"])[["scope", "used", "resets_at"]].to_string(index=False))

When an allowance is used up the server answers 429 and the client raises CausiloQuotaExceeded. The exception says which allowance it was, how much was used and when it resets, so the calling code can wait for the reset instead of retrying immediately. Too many requests in a minute or an hour produce the same status code with a different exception, CausiloRateLimited, which the client raises only after it has waited out Retry-After three times.

usage_limits_and_cancel.pyPYTHON
def predict_or_wait(context, query, **kw):
    try:
        return cx.predict(context, query, **kw)
    except CausiloQuotaExceeded as exc:
        print(f"{exc.scope} cap of {exc.limit:,} cells spent ({exc.used:,} used); resets at {exc.resets_at}")
        raise
    except CausiloRateLimited as exc:
        print(f"rate limited on the {exc.scope} window; resets at {exc.resets_at}")
        raise

Input the server refuses

Tables travel as Parquet. Datetime, timedelta, Period and decimal columns are refused with a message that names the column, because converting them silently would change what the model sees. Split dates into the parts that carry meaning for your problem.

usage_limits_and_cancel.pyPYTHON
d = load_diabetes(as_frame=True)
context = d.data.assign(progression=d.target)
query = d.data.iloc[:20]

visit = pd.date_range("2024-01-01", periods=len(context), freq="D")
try:
    cx.predict(context.assign(visit=visit), query.assign(visit=pd.Timestamp("2024-01-01")),
               target="progression", model="causilo-reg")
except CausiloError as exc:
    print("refused:", type(exc).__name__, "-", exc)

context2 = context.assign(visit_month=visit.month, visit_dow=visit.dayofweek)
query2 = query.assign(visit_month=1, visit_dow=0)
pred = cx.predict(context2, query2, target="progression", model="causilo-reg")
print("with the date split into parts:", len(pred), "predictions")

Giving up on a call

The client waits 900 seconds by default. If you set a shorter timeout and it expires, the client raises CausiloTimeout and does not retry, because the server is still computing and a retry would be counted a second time.

Giving up does not return the cells. The prediction runs behind an asynchronous endpoint that never learns that the caller disconnected, so it runs to the end and its cells are counted. The client asks the server to cancel, the server answers 501, and CausiloTimeout.refunded comes back None to show that the usage stands. Choose a timeout long enough that you will read the answer.

The table below is small to send and slow to compute: 60,000 context rows take the server well over five seconds, and this client gives up after five.

usage_limits_and_cancel.pyPYTHON
rng = np.random.default_rng(0)
context = pd.DataFrame(rng.normal(size=(60_000, 3)), columns=["f0", "f1", "f2"])
context["y"] = (context["f0"] + context["f1"] > 0).astype(int)
query = context.drop(columns="y").iloc[:100]

before = cx.usage()["used"]
impatient = Causilo(ENDPOINT, token=os.environ["CAUSILO_KEY"], timeout=5)
try:
    impatient.predict(context, query, target="y", model="causilo-clf")
except CausiloTimeout as exc:
    print("timed out; cells refunded:", exc.refunded)

The script waits a minute before reading /v1/usage, so that the prediction it abandoned has certainly finished.

usage_limits_and_cancel.pyPYTHON
time.sleep(60)
print("cells charged for the answer nobody read:", cx.usage()["used"] - before)

A request that fails is a different case and is not counted. A 413, a 422, a response too large to return and an inference that errors all release the cells before the reply goes out. A call you abandon is counted in full because the server never hears that you left, and the reference lists the server-side failures that are counted as well.

The exceptions the client raises:

Exception Status What to do
CausiloValidationError 400, 422 Fix the request. The message names the column or the parameter.
CausiloTooLarge 413 The message names a row count that fits. Nothing was counted.
CausiloQuotaExceeded 429 Wait for resets_at, or ask for a higher cap.
CausiloRateLimited 429 The client already waited. Slow the loop down.
CausiloOverloaded 429 Retry once the queue drains. Not the quota 429: this one carries no code.
CausiloUnavailable 503 The client already retried three times.
CausiloAuthError 401 Check the key.
CausiloTimeout 504 or none Not retried. The work continues and is counted; send a smaller table.

Every one carries .status, .error_code and .request_id, the identifier to quote when asking us about a call.

Output

stdout

Recorded stdout
limits: {'models': ['causilo-clf', 'causilo-reg'], 'output_types': {'classification': ['probas', 'preds'], 'regression': ['mean', 'median', 'quantiles']}, 'max_request_bytes': 1073741824, 'max_response_bytes': 536870912, 'max_rows': 300000, 'max_cols': 4096, 'max_features': 4095, 'max_cells': 60000000, 'max_classes': 64, 'max_inference_s': 300.0, 'context_max_age_s': 21600.0, 'key_cache_ttl_s': 60.0}
3,000,000 x 5 table: too many rows
 scope  used                 resets_at
caller 48840 2026-10-01T00:00:00+00:00
 daily 48840 2026-09-30T00:00:00+00:00
refused: CausiloValidationError - Datetime-like columns are not accepted: visit. Preprocessing drops them without warning. Convert to a number (epoch seconds) or a string. (request 20b4cd83f1924694)
with the date split into parts: 20 predictions
timed out; cells refunded: None
cells charged for the answer nobody read: 180300