Skip to content

New: deep research, balance and auto-reload tools in the keyless MCP server. Install in one line

/contents

Any URL, as clean Markdown for your agent

Fetch up to 10 pages per call, or put /r/ in front of any link. Add a question and get back only the passages that answer it.

Recorded 2026-10-17852 ms↓ 98.5% tokens
GETapi.contextfetch.dev/r/https://docs.python.org/3/library/asyncio-task.html?q=timeouts
text/markdown
HTTP/2 200
x-raw-tokens: 11188
x-output-tokens: 173
x-token-savings: 98.5

To actually run a coroutine, asyncio provides the following mechanisms:

- The `asyncio.run()` function to run the top-level
entry point “main()” function (see the above example.)
- Awaiting on a coroutine.  The following snippet of code will
print “hello” after waiting for 1 second, and then print “world”
after waiting for *another* 2 seconds:

```python3
import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)

Works with

exa_py and exa-js get_contents after a base-URL change, and the extract tool in our MCP server.

  • Claude CodeMCP, one line
  • CursorMCP server
  • WindsurfMCP server
  • exa_pychange base_url
  • tavily-pythonchange api_base_url
  • Any MCP clientstdio, keyless

Outputs purpose-built for your agents

Each page comes back in the shape your agent needs. The examples are from one recorded search.

The asyncio Timeout Trap: Why wait_for() Doesn't Stop Tasks Instantly and How to Fix Itrunebook.dev

I'll break down the common issues and offer sample code for alternatives in a friendly way! In Python's asyncio, timeouts are handled primarily using the asyncio.wait_for() utility. It wraps a coroutine or task and raises an asyncio.TimeoutError if the specified duration is exceeded. A frequent misunderstanding is that asyncio.wait_for() magically stops the task exactly at the timeout mark. The Reality When asyncio.wait_for() raises TimeoutError, the underlying task is cancelled, but it might not stop execution immediately. The task needs to notice the cancellation (by checking task.cancelled() or catching asyncio.CancelledError) and clean up gracefully. If the task is stuck in a synchronous I/O call or a heavy CPU loop, it won't notice the cancellation until it returns to the event loop. import asyncio async def long_running_operation(): print("Task started...") This task doesn't check

Results

Ranked URLs with title, published date and score, plus the full page text when you ask for it.

  • The asyncio Timeout Trap: Why wait_for() Doesn't Stop Tasks Instantly and How to Fix Itrunebook.dev

    I'll break down the common issues and offer sample code for alternatives in a friendly way! In Python's asyncio, timeouts are handled primarily using the asyncio.wait_for() utility. It wraps a coroutine or task and raises an asyncio.TimeoutError if the specified duration is exceeded. A frequent misunderstanding is that asyncio.wait_for() magically stops the task exactly at the timeout mark. The Reality

  • GitHub - aio-libs-abandoned/async-timeout: asyncio-compatible timeout class · GitHubgithub.com

    asyncio-compatible timeout context manager. This library has effectively been upstreamed into Python 3.11+. Therefore this library is considered deprecated and no longer actively supported. Version 5.0+ provides dual-mode when executed on Python 3.11+: asyncio_timeout.Timeout is fully compatible with asyncio.Timeout and old versions of the library. Anyway, using upstream is highly recommended. asyncio_timeout exists only for the sake of backward compatibility, easy supporting both old and new Python by the same code, and easy misgration. If rescheduling API is not important and only async with timeout(...): ... functionality is required, a user could apply conditional import:

  • The async with timeout: Best Practices for Robust Python Asynchronous I/Orunebook.dev

    Here's a friendly breakdown of the asyncio.timeout() context manager, common pitfalls, and reliable alternatives. The asyncio.timeout() function provides a way to impose a time limit on an awaitable operation (like an asynchronous function, a task, or an I/O operation) within a specific block of code. It works as an async context manager When you enter the async with asyncio.timeout(delay): block, a timer starts. If the code inside the block finishes before the time limit (delay) expires, everything is normal.

  • Python asyncio.wait_for(): Cancel a Task with a Timeoutpythontutorial.net

    import asyncio from asyncio.exceptions import TimeoutError async def call_api(message, result=1000, delay=3): print(message) await asyncio.sleep(delay) return result async def main(): task = asyncio.create_task( call_api('Calling API...', result=2000, delay=5) ) MAX_TIMEOUT = 3 try: await asyncio.wait_for(task, timeout=MAX_TIMEOUT) except TimeoutError: print('The task was cancelled due to a timeout') asyncio.run(main()) Copy Output: Calling API... The task was cancelled due to a timeoutCopy How it works First, define a call_api() coroutine that takes 3 seconds to complete by default:

Highlights

The passages that match the query, ranked and scored, ready to quote.

Included in the credit
prunedContext5,322 → 2,305 tokens

asyncio-compatible timeout context manager. This library has effectively been upstreamed into Python 3.11+. Therefore this library is considered deprecated and no longer actively supported. Version 5.0+ provides dual-mode when executed on Python 3.11+: asyncio_timeout.Timeout is fully compatible with asyncio.Timeout and old versions of the library. Anyway, using upstream is highly recommended. asyncio_timeout exists only for the sake of backward compatibility, easy supporting both old and new Python by the same code, and easy misgration. If rescheduling API is not important and only async with timeout(...): ... functionality is required, a user could apply conditional import: if sys.version_info >= (3, 11): from asyncio import timeout, timeout_at else: from async_timeout import timeout, timeout_at The context manager is useful in cases when you want to apply timeout logic around block of code or in cases when asyncio.wait_for() is not suitable. Also it's much faster than asyncio.wait_for() because timeout doesn't create a new task. The timeout(delay) call returns a context manager that cancels a block on timeout expiring: from async_timeout import timeout async with timeout(1.5): await inner() async with timeout(1.5) as cm: recommended api cm.reschedule(cm.when() + 1) add another second on waiting compatible api cm.shift(1) add another second on waiting cm.update(loop.time() + 5) reschedule to now+5 seconds Rescheduling is forbidden if the timeout is expired or after exit from async with code block. Disable scheduled timeout: async with timeout(1.5) as cm: cm.reschedule(None) recommended api cm.reject() compatible api $ pip install async-timeout The library is Python 3 only! The module is written by Andrew Svetlov. It's Apache 2 licensed and freely available.

github.com

Pruned context

A cross-encoder keeps only what answers the query: 57% fewer tokens on this search.

Saves LLM tokens
runebook.dev
# The asyncio Timeout Trap: Why wait\_for() Doesn't Stop Tasks Instantly and How to Fix It

I'll break down the common issues and offer sample code for alternatives in a friendly way!

In Python's `asyncio`, timeouts are handled primarily using the `asyncio.wait_for()` utility. It wraps a coroutine or task and raises an `asyncio.TimeoutError` if the specified duration is exceeded.

A frequent misunderstanding is that `asyncio.wait_for()` magically stops the task exactly at the timeout mark.

The Reality

When `asyncio.wait_for()` raises `TimeoutError`, the underlying task is cancelled, but it might not stop execution immediately. The task needs to notice the cancellation (by checking `task.cancelled()` or catching `asyncio.CancelledError`) and clean up gracefully. If the task is stuck in a synchronous I/O call or a heavy CPU loop, it won't notice the cancellation until it returns to the event loop.

```python
import asyncio

async def long_running_operation():
    print("Task started...")
    # This task *doesn't* check for cancellation or yield control
    # It will run until the en

Markdown

Clean page Markdown with headings, tables and code blocks kept, not raw HTML.

Included in the credit

Two ways in

An API for batches, a URL for everything else

POST /v1/contents
{
  "urls": [
    "https://docs.python.org/3/library/asyncio-task.html"
  ],
  "text": true,
  "highlights": {
    "query": "timeouts"
  }
}

POST /v1/contents

Up to 10 URLs per call with text, highlights and pruned context. Exa's request and response shapes.

Reader rules
  • /r/<any page>→ Markdown, tables and code kept
  • /r/<page>?q=<question>→ only the relevant chunks
  • /r/github.com/…/blob/…→ the raw file
  • /r/arxiv.org/pdf/…→ the HTML version

GET /r/{url}

The Reader: any public URL as text/markdown, with token counts in the response headers.

Fewer tokens per page

Context left after pruning, as a share of the full page Markdown. Two real requests recorded for this site, and our offline benchmark.

Context tokens as a percentage of the full page
GroupFull pagePruned
Reader, ?q=timeouts100.0%1.5%
Search, 5 pages100.0%43.3%
Synthetic docs, mean100.0%30.5%

Reader: GET /r/https://docs.python.org/3/library/asyncio-task.html?q=timeouts, recorded 2026-10-17. Search: the recorded “python asyncio timeout” query with pruned context. Synthetic docs: 29 query/document pairs written for this repo (benchmarks/results.json), not live pages. How much pruning saves depends on the page and the question.

Quickstart

Read a page in one call

Use the Exa SDK you already have, call the endpoint, or let your agent use the MCP tool.

  • Up to 10 URLs per call, failed ones not billed
  • Query-aware highlights and pruned context
  • No key needed to try it

Endpoint reference

# pip install exa-pyfrom exa_py import Exa exa = Exa(api_key="YOUR_API_KEY", base_url="https://api.contextfetch.dev") res = exa.get_contents(    ["https://docs.python.org/3/library/asyncio-task.html"],    text=True,    highlights={"query": "timeouts"},)print(res.results[0].highlights)

Pricing

Contents API for your agents

$1.00/ 1k pages

1 credit per URL that loads, the same for the Reader. Failed URLs are not billed. One-time payment. Credits never expire. Full Markdown and pruning included.

Questions

What does it cost?

POST /v1/contents costs 1 credit per URL that loads, up to 10 URLs per call; failed URLs come back in statuses and are not billed. A Reader page (GET /r/{url}) is 1 credit. That is $1.00 per 1,000 pages on the base pack.

What can the Reader read?

Any public http(s) page. GitHub blob URLs are read as raw files and arXiv PDFs as their HTML versions. Fetch limits: http/https only, at most 5 redirects, 5 MB body, 15 s timeout.

Can it fetch private or internal addresses?

No. Private, loopback, link-local and similar addresses are rejected with 400 url_not_allowed, also when a public URL redirects to one.

What are highlights and pruned context?

Highlights are the 400 to 700 character Markdown chunks that best match your query, with scores. Pruned context keeps only the passages a cross-encoder rates relevant, so your agent reads fewer tokens. numSentences is accepted for Exa compatibility and ignored.

What do the Reader's response headers mean?

X-Raw-Tokens is the page size before, X-Output-Tokens what you got back (both ceil(characters / 4)), and X-Token-Savings the percentage saved: Markdown versus raw HTML, or pruned versus full Markdown when you pass ?q=.

Stop pasting whole pages into context

25 free requests a day with no key. Then $1.00 per 1,000 pages.