Timeouts and deadlines
Use `coco.timeout()` to put a time budget around a piece of processing — one file, one LLM call — with cooperative checks, deadline-aware retries, and clean time-boxing of whole updates.
Use coco.timeout() when a piece of CocoIndex work should stop after a time budget.
The timeout is cooperative: CocoIndex checks the time at safe points and raises coco.DeadlineExceededError when the deadline has passed. Enforcement is best-effort beyond that — CocoIndex may additionally cancel an in-flight await where that is known to be safe (the built-in retry helpers do), but your own code inside an await, such as an HTTP request or database query, is generally not interrupted. Use that library’s own timeout for individual I/O calls.
Quick start
The most common budget is per item of work inside a processing component — one file, one LLM call. Per-item budgets stay meaningful as your source grows (a whole-run limit does not), and they apply no matter how the app runs, including via the cocoindex update CLI:
from datetime import timedelta
import cocoindex as coco
@coco.fn(memo=True)
async def process_file(file: FileLike, target: localfs.DirTarget) -> None:
# This file gets 30 seconds, including retries inside.
with coco.timeout(timedelta(seconds=30)):
summary = await summarize_with_llm(await file.read_text())
target.declare_file(filename=out_name(file), content=summary)
If the budget runs out, the component fails with coco.DeadlineExceededError like any other processor error: nothing external is written for it, nothing is memoized, and the next update retries it cleanly.
Nested timeout blocks use the shorter remaining budget:
with coco.timeout(timedelta(minutes=5)):
...
with coco.timeout(timedelta(minutes=30)):
# Still bounded by the outer 5-minute timeout.
...
with coco.timeout(timedelta(seconds=10)):
# Temporarily narrowed to 10 seconds.
...
# Back to the outer timeout.
The rule
A timeout follows work the caller is directly waiting for. It does not follow background work or shared work.
| Code shape | What happens |
|---|---|
with coco.timeout(...): app.update() | The root processor inherits the timeout (see Time-boxing a whole update). |
Direct @coco.fn call | Inherits the current timeout. |
await coco.use_mount(child, ...) | The child inherits the timeout because the parent needs the child’s result. |
await coco.mount(child, ...) / await coco.mount_each(...) | The child does not inherit the parent’s timeout. The caller’s own waiting remains subject to the caller’s timeout. |
await coco.map(fn, items) | Item tasks inherit the timeout. Already-started items are allowed to finish. |
| Batched function body | Does not inherit any one caller’s timeout because a batch can contain work from multiple callers. |
| Target writes and connector sink calls | Do not inherit the processor timeout. Once processing has succeeded, CocoIndex lets target synchronization finish consistently. |
| Long loop inside a processor | Add coco.check_cancellation() where you want the loop to stop promptly. |
Foreground child work
Use use_mount() when the parent depends on the child’s value. The timeout applies to both parent and child:
@coco.fn
async def parse_file(file: FileLike) -> ParsedDoc:
text = await file.read_text()
coco.check_cancellation()
return parse(text)
@coco.fn
async def main(file: FileLike) -> None:
parsed = await coco.use_mount(
coco.component_subpath("parse"),
parse_file,
file,
)
index(parsed)
with coco.timeout(timedelta(seconds=30)):
app.update_blocking()
If the deadline passes while parse_file() is in file.read_text(), CocoIndex does not interrupt that await. The timeout is observed when control reaches the next CocoIndex check, such as coco.check_cancellation() or the function return.
Background child work
Use mount() or mount_each() when the child is independent background work. The parent’s timeout can stop the parent from waiting forever, but it does not become the child’s timeout:
@coco.fn
async def process_file(file: FileLike) -> None:
# No timeout is inherited from the parent mount() call.
...
@coco.fn
async def main(files: list[FileLike]) -> None:
with coco.timeout(timedelta(seconds=30)):
handle = await coco.mount(
coco.component_subpath("first-file"),
process_file,
files[0],
)
# This wait is controlled by the parent's timeout.
# The child processor itself remains independent.
await handle.ready()
If the parent times out while waiting for handle.ready(), the timeout belongs to the parent wait. It does not retroactively cancel the child processor.
What happens on timeout
The timing contract is deliberately loose:
- Any CocoIndex-managed call may raise
coco.DeadlineExceededErrorif the deadline passes before it returns. - CocoIndex checkpoints —
coco.check_cancellation(), calling a@coco.fn, engine calls such ascoco.use_mount()— guarantee to raise if the deadline has already passed when they are called.
Don’t rely on the exact point where the error is raised. It can move between versions, and enforcement can get tighter where cancelling is safe (the built-in retry helpers already cancel an in-flight attempt at the deadline).
What you can rely on is the outcome:
- A processor run that times out applies no target writes and memoizes nothing; the next update retries it cleanly.
- Completed work stays completed: committed components, finished
coco.map()items, and in-progress target synchronization are never rolled back or interrupted by a timeout.
Add checks in long processors
CocoIndex already checks common boundaries, but a long processor loop should add explicit checks:
@coco.fn
async def import_rows(rows: list[Row]) -> None:
for row in rows:
target.declare_row(row=transform(row))
coco.check_cancellation()
This makes the loop stop near the deadline instead of waiting until the whole function returns.
Long I/O still needs its own timeout
coco.timeout() does not replace client-level timeouts:
@coco.fn
async def fetch_page(client: httpx.AsyncClient, url: str) -> str:
# httpx controls this individual request.
response = await client.get(url, timeout=10.0)
# CocoIndex controls the orchestration around it.
coco.check_cancellation()
return response.text
Think of the two budgets as complementary:
- client timeouts bound one external call
coco.timeout()bounds CocoIndex work between safe checks
Time-boxing a whole update (programmatic callers)
When you call app.update() yourself and the surrounding environment has its own clock (a CI job, a serverless handler, a shared cron window), you can put the budget around the whole update:
try:
with coco.timeout(timedelta(minutes=10)):
app.update_blocking()
except coco.DeadlineExceededError:
print("stopped after the 10-minute budget")
In async code, create the update handle inside the timeout block — the deadline is captured when app.update() creates the handle:
with coco.timeout(timedelta(minutes=10)):
handle = app.update()
await handle.result()
Because components commit independently and the engine rolls forward, a whole-update timeout never wastes completed work: everything that finished is committed and stays committed; the timeout only stops new progress, and the next update resumes where this one left off. with coco.timeout(timedelta(minutes=10)): app.update() means “index as much as possible in ten minutes, cleanly.”
Retries
Retry helpers distinguish two kinds of limit:
- Attempt caps owned by the call site — exhausting one re-raises the last transient error, so your normal error handling still applies.
- Deadlines — the one time concept. A retry operation’s own time limit is just a nested
coco.timeout(...)scope, so it merges with any ambient deadline (the shorter wins) and exhaustion raisescoco.DeadlineExceededError.
Adding a timeout therefore never increases retry effort, and removing one never decreases it. No retry attempt starts past any limit, no result is accepted past the deadline, and backoff sleeps never exceed the remaining time. Built-in operations (LlmPairResolver, LiteLLM embedding retries) share one helper with these semantics.
API
with coco.timeout(datetime.timedelta(seconds=30)):
...
Sets a cooperative timeout for the current CocoIndex context.
coco.check_cancellation()
Raises coco.DeadlineExceededError if the current deadline has passed.
class coco.DeadlineExceededError(TimeoutError):
...
Raised when a CocoIndex timeout is exceeded.