# Functions
source: https://docs.chalk.ai/docs/compute/functions

## Define and call remote functions that run on Chalk Compute.

### Overview

A Chalk Function is a Python callable that you deploy onto a
Scaling Group and register in the
Chalk Catalog. Decorate a function
with @chalkcompute.function and it becomes reachable from anywhere Chalk
runs: local scripts and notebooks, other functions, and (because of the
Catalog registration) the feature engine, including Chalk SQL, resolvers, the
query server, and DataFrames via F.catalog_call(). Arguments and results are
serialized automatically; you write ordinary Python and Chalk handles
scheduling, scaling, and delivery.

On top of what a bare Scaling Group provides, functions add async / streaming
invocation, nested calls, automatic retries, concurrency caps, rate limits,
and request-queue-driven autoscaling. Reach for a function when you want any
of those features, or when you want the callable to be visible to the rest
of Chalk.

Functions support three return styles:

- Single value: return a result and the caller receives it.
- Async generator: yield results incrementally and the caller receives them as
they're produced. See Streaming results
for an end-to-end example.
- Nested calls: a function can call other functions, composing pipelines without
manual orchestration. See Nested function calls
for a pipeline example.

To define and deploy a function, see Defining a function.

### How it works

@chalkcompute.function is backed by Chalk Compute's async execution layer.
At a high level, calling a function goes through three moving parts:

```
┌──────────┐         ┌──────────────────┐         ┌───────────┐
│  Caller  │──call──▸│  Function Queue  │──run───▸│  Worker   │
│          │◂results─│                  │◂results─│ (Python)  │
└──────────┘         └──────────────────┘         └───────────┘
```

- The caller (local script, notebook, another function) submits an
invocation and receives results over a single bidirectional stream.
- The Function Queue durably accepts the work, assigns each call an ID,
enforces configured policies (concurrency, rate limit, retry), and relays
results back to the caller.
- The worker is the Python process that actually runs your function code.

Callers and workers are loosely coupled: the caller never opens a connection
directly to a worker. Workers can scale up, scale down, or get rescheduled
mid-flight and in-flight calls are not lost. This is what enables streaming
async generators, nested calls, and policy-enforced dispatch.

### Policy enforcement

The queue enforces the policies declared on each function (rate limit,
concurrency, and retries) before dispatching work to the worker.

| Policy                                                                          | What it caps                                                                                                                                                                |
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`rate_limit`](/docs/functions/rate-and-concurrency-limits#rate-limiting)       | Outbound requests per second / minute / hour, optionally shared across functions via a `key`.                                                                               |
| [`concurrency`](/docs/functions/rate-and-concurrency-limits#concurrency-limits) | In-flight invocations at once, optionally shared across functions via a `key`. Defaults to `1` per function.                                                                |
| [`retries`](/docs/functions/retries)                                            | Automatic retry of transient failures (`UNAVAILABLE`, `DEADLINE_EXCEEDED`, `RESOURCE_EXHAUSTED`, `ABORTED`) with exponential backoff and jitter. Other errors are terminal. |

Concurrency is acquired before dispatching, so a burst of queued calls
backpressures on the semaphore rather than spawning unbounded tasks.
Rate-limit tokens are consumed per attempt: each retry takes a fresh token.

### Retry safety

Retries are safe for failures that happen before the function has started
streaming results: connection errors, DEADLINE_EXCEEDED on the initial
call, etc. If a function has already yielded chunks when it fails, those
chunks are visible to the caller and a retry would produce an ambiguous
stream. Retries therefore apply cleanly only to pre-stream failures today;
making mid-stream retries safe is a known follow-up.





