# Timeouts and deadlines

> **CocoIndex v1.** This page documents CocoIndex **v1** — a ground-up redesign from v0. When writing code, ignore any v0 flow-builder DSL or deprecated decorators.
>
> Source: https://cocoindex.io/docs/advanced_topics/timeouts/ · Docs index: https://cocoindex.io/docs/llms.txt · Agent skill: https://cocoindex.io/docs/skill.md
>
> v0→v1 quick map — if you reach for these v0 symbols, stop and use the v1 form: `@cocoindex.flow_def`/`FlowBuilder` → `coco.App` + a `@coco.fn` main function; `add_collector()`/`collect()`/`export()` → declare target states (`declare_row`, `declare_file`); `cocoindex.sources/functions/targets.*` → connector APIs (`localfs.walk_dir`, `coco.ops.*`, `postgres.declare_table_target`). Full mapping + API reference: https://cocoindex.io/docs/skill.md.

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:

```python
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:

```python
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](#time-boxing-a-whole-update-programmatic-callers)). |
| 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:

```python
@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:

```python
@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.DeadlineExceededError` if the deadline passes before it returns.
- CocoIndex checkpoints — `coco.check_cancellation()`, calling a `@coco.fn`, engine calls such as `coco.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:

```python
@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:

```python
@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:

```python
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:

```python
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 raises `coco.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

```python
with coco.timeout(datetime.timedelta(seconds=30)):
    ...
```

Sets a cooperative timeout for the current CocoIndex context.

```python
coco.check_cancellation()
```

Raises `coco.DeadlineExceededError` if the current deadline has passed.

```python
class coco.DeadlineExceededError(TimeoutError):
    ...
```

Raised when a CocoIndex timeout is exceeded.
