Usage, limits and cancellation

Three things the API tells you that are worth reading before you build on it: what it will accept, what a call costs, and what happens to the charge when you give up on a call.

pip install -U causilo-client scikit-learn requests
python usage_limits_and_cancel.py
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["CAUSILO_ENDPOINT"]
cx = Causilo(ENDPOINT, token=os.environ["CAUSILO_KEY"])

Limits, before you send anything

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

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

One cell of the table you sent, context and query together; a 1,000 by 21 context with a 200 by 20 query is 24,000 cells. The month and the day each have a cap, and usage() reads both without spending anything.

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

When a cap is spent the server answers 429 and the client raises CausiloQuotaExceeded. The exception carries which cap it was, the figures, and when it resets, so the calling code can wait for the day to turn rather than retry into the same wall. A minute or hour of too many requests is the same status code but a different exception, CausiloRateLimited; the client has already waited out Retry-After three times before it lets that one through.

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 naming the column, rather than being silently turned into something the model treats as a number. Split dates into the parts that carry meaning for your problem.

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: the server is still computing, and a retry would be charged for work that finishes anyway.

Giving up does not save you the cells. The prediction runs behind an asynchronous endpoint that never learns you hung up, so it runs to the end and the cells are charged. The client asks the server to cancel, the server answers 501, and CausiloTimeout.refunded comes back None -- the client saying the charge stands. Pick a timeout you are willing to pay for.

The table below is small on the wire and slow to compute: 60,000 context rows take the server about twenty seconds, and the client gives up after five.

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 charge lands when the prediction finishes, which is after you stopped waiting for it. Reading /v1/usage the instant the timeout fires shows nothing.

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 charged: a 413, a 422, a response too large to return, an inference that errors. Those release the cells before the reply goes out. It is only a call you abandon, which the server never hears about, that you pay for in full.

Every error, in one place:

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

limits: {'models': ['causilo-clf', 'causilo-reg'], 'output_types': {'classification': ['probas', 'preds'], 'regression': ['mean', 'median', 'quantiles']}, 'max_request_bytes': 1073741824, 'max_response_bytes': 33554432, '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    limit  used  remaining                 resets_at
caller 25000000 48840   24951160 2026-10-01T00:00:00+00:00
 daily  6250000 48840    6201160 2026-09-19T00: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 a753096e407ee4ed)
with the date split into parts: 20 predictions
timed out; cells refunded: None
cells charged for the answer nobody read: 180300

Recorded 2026-09-18 · image 0.12.16