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