Resource limits
jed meters the execution cost of every query deterministically — the same query against the same database always costs the same, on every core. Two ceilings turn that meter into the resource half of the “untrusted SQL is safe to run” guarantee (the Authorization page is the privilege half). Pair them and you can hand an adversary a query surface.
Two ceilings
max_cost— per statement (54P01). A ceiling on a single statement: the instant a query’s accrued cost reaches it, execution aborts with54P01. This stops one runaway query — a cross join, a giantgenerate_series, an expensive expression over a huge input.lifetime_max_cost— per session (54P02). A budget on the whole session’s cumulative cost. The session holds a running total into which every statement accrues; the instant that total reaches the budget, the in-flight statement aborts with54P02. This stops a flood of cheap statements that each slip undermax_costbut together burn unbounded CPU.
Both default to 0 (unlimited). A statement aborts at whichever ceiling it reaches first.
use jed::{CreateOptions, Database, SessionOptions};
fn main() -> jed::Result<()> {
let mut db = Database::create(CreateOptions { path: Some("app.jed".into()), ..Default::default() })?;
// Serve untrusted queries through a session bounded TWO ways:
// max_cost — a per-STATEMENT ceiling: one runaway query aborts 54P01.
// lifetime_max_cost — a per-SESSION budget: the session's cumulative cost is capped, so a
// flood of cheap queries can't burn unbounded CPU. It aborts 54P02.
let mut untrusted = db.session(SessionOptions {
max_cost: 10_000,
lifetime_max_cost: 3, // tiny, for illustration
..SessionOptions::default()
});
// Each statement accrues into the session's running total; read it with lifetime_cost().
untrusted.execute("SELECT 1", &[])?; // cost 1 — cumulative 1
untrusted.execute("SELECT 1", &[])?; // cost 1 — cumulative 2
// The third drives the cumulative to the budget — the in-flight statement aborts 54P02, and the
// partial cost still counts, so the session is now spent.
let denied = untrusted.execute("SELECT 1", &[]);
assert_eq!(denied.unwrap_err().code(), "54P02");
assert_eq!(untrusted.lifetime_cost(), 3);
// Once spent, every further statement is rejected at admission — the session is done.
let after = untrusted.execute("SELECT 1", &[]);
assert_eq!(after.unwrap_err().code(), "54P02");
untrusted.close();
Ok(())
}
package main
import (
"context"
"fmt"
"log"
jed "github.com/jackc/jed/impl/go"
)
func main() {
db, err := jed.CreateDatabase(jed.CreateOptions{Path: "app.jed"})
if err != nil {
log.Fatal(err)
}
defer db.Close()
ctx := context.Background()
// Serve untrusted queries through a session bounded TWO ways:
// MaxCost — a per-STATEMENT ceiling: one runaway query aborts 54P01.
// LifetimeMaxCost — a per-SESSION budget: the session's cumulative cost is capped, so a flood
// of cheap queries can't burn unbounded CPU. It aborts 54P02.
untrusted := db.Session(jed.SessionOptions{MaxCost: 10000, LifetimeMaxCost: 3})
defer untrusted.Close()
// Each statement accrues into the session's running total; read it with LifetimeCost().
untrusted.Exec(ctx, "SELECT 1") // cost 1 — cumulative 1
untrusted.Exec(ctx, "SELECT 1") // cost 1 — cumulative 2
// The third drives the cumulative to the budget — the in-flight statement aborts 54P02, and the
// partial cost still counts, so the session is now spent.
if _, err := untrusted.Exec(ctx, "SELECT 1"); err != nil {
fmt.Println("denied:", err.(*jed.EngineError).Code()) // 54P02
}
fmt.Println("spent:", untrusted.LifetimeCost()) // 3 — the budget
// Once spent, every further statement is rejected at admission — the session is done.
if _, err := untrusted.Exec(ctx, "SELECT 1"); err != nil {
fmt.Println("admission:", err.(*jed.EngineError).Code()) // 54P02
}
}
import { createDatabase, EngineError } from 'jed-ts';
const db = createDatabase({ path: 'app.jed' });
// Serve untrusted queries through a session bounded TWO ways:
// maxCost — a per-STATEMENT ceiling: one runaway query aborts 54P01.
// lifetimeMaxCost — a per-SESSION budget: the session's cumulative cost is capped, so a flood of
// cheap queries can't burn unbounded CPU. It aborts 54P02.
const untrusted = db.session({ maxCost: 10000n, lifetimeMaxCost: 3n });
// Each statement accrues into the session's running total; read it with lifetimeCost().
untrusted.execute('SELECT 1'); // cost 1 — cumulative 1
untrusted.execute('SELECT 1'); // cost 1 — cumulative 2
// The third drives the cumulative to the budget — the in-flight statement aborts 54P02, and the
// partial cost still counts, so the session is now spent.
try {
untrusted.execute('SELECT 1');
} catch (e) {
if (e instanceof EngineError) console.log('denied:', e.code()); // 54P02
}
console.log('spent:', untrusted.lifetimeCost()); // 3n — the budget
// Once spent, every further statement is rejected at admission — the session is done.
try {
untrusted.execute('SELECT 1');
} catch (e) {
if (e instanceof EngineError) console.log('admission:', e.code()); // 54P02
}
untrusted.close();
db.close();
How the budget behaves
- The partial cost of an aborted statement still counts. The work happened, so it is charged — reaching the budget genuinely spends it.
- Once spent, the session is done. Every further statement is rejected
54P02at admission, before it can run (so a missing-table query under an exhausted budget is54P02, not42P01). - The cumulative is session state, not data. It does not roll back when a transaction rolls back — the compute was spent regardless. Read it any time with the cumulative-cost gauge.
This is the clean “this session has a total compute allowance” model for a multi-tenant or untrusted-query host: a session granted only the privileges it needs, capped per statement, and budgeted over its lifetime.