diff --git a/docs/concepts/https-outcalls.md b/docs/concepts/https-outcalls.md index a85aefa8..862f659c 100644 --- a/docs/concepts/https-outcalls.md +++ b/docs/concepts/https-outcalls.md @@ -83,7 +83,7 @@ Version 1 is still the default, and is what a call gets unless it asks for versi ::: -Both the Motoko `ic` mops package and the Rust `ic-cdk-management-canister` crate provide wrappers that compute and attach the required amount. In Rust, the `HttpRequest` and `FlexibleHttpRequest` builders always price with version `2`, using the `ic0.cost_http_request_v2` system API. In Motoko, `Call.httpRequest` prices with version `1` using `ic0.cost_http_request`, so a canister that wants version `2` or flexible mode from Motoko has to build the management canister call itself for now. Do not set `pricing_version = 2` on a request passed to `Call.httpRequest` once its argument type carries the field: the wrapper would still attach the version `1` amount, which is far below the version `2` reservation, and the call would run within a budget too small to finish. +Both languages provide a wrapper that computes and attaches the required amount, but not for the same version: the Rust builders in `ic-cdk-management-canister` always price with version `2`, while Motoko's `Call.httpRequest`, from the `ic` mops package, still prices with version `1`. Reaching version `2` or flexible mode from Motoko therefore means building the management canister call yourself for now. The [HTTPS outcalls guide](../guides/backends/https-outcalls.md#cycle-costs) covers what each wrapper attaches and the pitfalls of overriding it. **Version 1** charges based on the following two factors: diff --git a/docs/guides/backends/https-outcalls.mdx b/docs/guides/backends/https-outcalls.mdx index 602d213d..f9ceda75 100644 --- a/docs/guides/backends/https-outcalls.mdx +++ b/docs/guides/backends/https-outcalls.mdx @@ -10,7 +10,7 @@ import CodeExample from '../../../src/components/CodeExample.astro'; [Canisters](../../concepts/canisters.md) can make HTTP requests to external web services using HTTPS outcalls. This lets your canister call REST APIs or send notifications: all from canister code. -HTTPS outcalls are available through the [IC management canister](../../references/management-canister.md) (`aaaaa-aa`) via the `http_request` method, and via `flexible_http_request` for the flexible mode described below. `GET`, `HEAD`, and `POST` are supported in every mode. `PUT`, `DELETE`, and `PATCH` are only supported where the number of requests and responses is fixed and known, i.e. non-replicated outcalls and flexible outcalls where the number of total requests, min responses, and max responses are equal. `HEAD` works identically to `GET` but returns only headers: useful for checking resource availability without downloading the body. Only HTTPS (not plain HTTP) is supported. +HTTPS outcalls are available through the [IC management canister](../../references/management-canister.md) (`aaaaa-aa`): the `http_request` method, and `flexible_http_request` for the flexible mode described below. `GET`, `HEAD`, and `POST` are supported in every mode; `PUT`, `DELETE`, and `PATCH` only in some (see [Outcall modes](#outcall-modes)). `HEAD` works identically to `GET` but returns only headers: useful for checking resource availability without downloading the body. Only HTTPS (not plain HTTP) is supported. For how the consensus mechanism works for outcalls, see [Concepts: HTTPS Outcalls](../../concepts/https-outcalls.md). @@ -31,6 +31,7 @@ HTTPS outcalls have three modes. Replicated and non-replicated are selected by t |---|---|---|---| | Who sends the request | All N nodes on the subnet | One node | A committee of `total_requests` nodes | | What the canister gets | One agreed response | The one node's response | Between `min_responses` and `max_responses` individual responses | +| Methods | `GET`, `HEAD`, `POST` | plus `PUT`, `DELETE`, `PATCH` | plus `PUT`, `DELETE`, `PATCH` when `total_requests`, `min_responses`, and `max_responses` are equal | | Consensus on response | Yes | No | No: consensus is on which responses to deliver | | Transform needed | Strongly recommended | Optional | Optional | | Pricing | Version 1 (deprecated) or 2 | Version 1 (deprecated) or 2 | Always version 2 | @@ -188,7 +189,7 @@ Version 1 is still the default, so a call that does not set `pricing_version` ge | Attaching too little | rejected up front | rejected up front if it misses the base fee; otherwise runs with tighter per-node limits and may fail partway | | Flexible outcalls | not available | required | -Which version a call gets depends on the CDK, and both compute the amount to attach for you. In Rust, the `HttpRequest` builder always selects version `2` and prices the call with `ic0.cost_http_request_v2`; `with_expected_*` narrows the reservation from the worst case to what the call is expected to consume. In Motoko, `Call.httpRequest` from the `ic` package attaches the version `1` cost using `ic0.cost_http_request`. The Rust examples above are therefore priced with version `2`, and the Motoko ones with version `1`. Do not set `pricing_version = 2` on a request you pass to `Call.httpRequest`: the wrapper would still attach the version `1` amount, which is far below what version `2` reserves, so the call would run within a budget too small to finish. Attaching a hand-picked amount instead is counterproductive: the cycles are held for the duration of the call, so a margin caps how many outcalls the canister can have in flight. +Which version a call gets depends on the wrapper you call through, and both compute the amount to attach for you. In Rust, the `HttpRequest` builder always selects version `2` and prices the call with `ic0.cost_http_request_v2`; `with_expected_*` narrows the reservation from the worst case to what the call is expected to consume. In Motoko, `Call.httpRequest` from the `ic` package attaches the version `1` cost using `ic0.cost_http_request`. The Rust examples above are therefore priced with version `2`, and the Motoko ones with version `1`. Do not set `pricing_version = 2` on a request you pass to `Call.httpRequest`: the wrapper would still attach the version `1` amount, which is far below what version `2` reserves, so the call would run within a budget too small to finish. Attaching a hand-picked amount instead is counterproductive: the cycles are held for the duration of the call, so a margin caps how many outcalls the canister can have in flight. ### Version 1 @@ -224,6 +225,8 @@ See [Cycles costs](../../references/cycle-costs.md#https-outcalls) for the full Use the "Full example in ICP Ninja" links above to deploy and test directly in the browser. To test locally with icp-cli, clone the example and run `icp network start -d && icp deploy`. > **Note:** The local replica runs a single node, so all responses reach consensus automatically: even without a transform function. Verify your transform produces identical output for varying inputs (different headers, timestamps) before deploying to a multi-node subnet, where mismatches cause "no consensus" errors. +> +> Flexible outcalls do run locally, but every response comes from that same single node, so a local run exercises the call and your reconciliation code without ever producing the disagreement reconciliation exists for. ## Next steps @@ -234,4 +237,4 @@ Use the "Full example in ICP Ninja" links above to deploy and test directly in t - [Chain Fusion: Ethereum](../chain-fusion/ethereum.md): the EVM RPC canister uses HTTPS outcalls under the hood - [Cycles costs](../../references/cycle-costs.md#https-outcalls): outcall pricing details -{/* Upstream: informed by dfinity/portal docs/building-apps/network-features/using-http/https-outcalls/; dfinity/examples send_http_get, send_http_post */} +{/* Upstream: informed by dfinity/portal docs/building-apps/network-features/using-http/https-outcalls/; dfinity/examples send_http_get, send_http_post, send_http_flexible */} diff --git a/docs/references/cycle-costs.md b/docs/references/cycle-costs.md index 46f65098..2c5b2e10 100644 --- a/docs/references/cycle-costs.md +++ b/docs/references/cycle-costs.md @@ -161,13 +161,18 @@ delivery_fee = n * (10 * n + 600) * (response_bytes (+ 181 * K flexible only)) (+ (2_000 * n + 100_000) * n * (K - min_responses) flexible only) ``` -`usage_fee` is charged for each node that performs the outcall: all `n` of them for a fully replicated call, one for a non-replicated call, `total_requests` for a flexible one. `response_bytes` is the size after the transform. A non-replicated call (`is_replicated = false`) takes the `otherwise` branch with `min_responses = 1`. A flexible call that does not set `replication` defaults `min_responses` to `floor(2 / 3 * n) + 1`. +`usage_fee` is charged for each node that performs the outcall: all `n` of them for a fully replicated call, one for a non-replicated call, `total_requests` for a flexible one. The `13` dividing `transform_instructions` is not the node count: outcall fees are calibrated against a reference subnet size of 13, and this is the only term that carries that constant, so a node is charged the same for a transform on every subnet. `response_bytes` is the size after the transform. A non-replicated call (`is_replicated = false`) takes the `otherwise` branch with `min_responses = 1`. A flexible call that does not set `replication` defaults `min_responses` to `floor(2 / 3 * n) + 1`. | Component | 13-node cycles | ~USD | 34-node cycles | ~USD | |-----------|----------------|------|----------------|------| -| Per fully replicated call (base) | 38_417_600 | ~$0.0000525 | 227_283_200 | ~$0.000311 | -| Per request byte | 650 | ~$0.0000000009 | 1_700 | ~$0.0000000023 | -| Per delivered response byte, charged | 9_490 | ~$0.0000000130 | 31_960 | ~$0.0000000437 | +| Base, per fully replicated call | 38_417_600 | ~$0.0000525 | 227_283_200 | ~$0.000311 | +| Base, per request byte | 650 | ~$0.0000000009 | 1_700 | ~$0.0000000023 | +| Usage, per raw response byte | 650 | ~$0.0000000009 | 1_700 | ~$0.0000000023 | +| Usage, per millisecond of round trip | 3_900 | ~$0.0000000053 | 10_200 | ~$0.0000000139 | +| Usage, per million transform instructions | ~1_000_000 | ~$0.0000014 | ~2_615_000 | ~$0.0000036 | +| Delivery, per delivered response byte | 9_490 | ~$0.0000000130 | 31_960 | ~$0.0000000437 | + +The three usage rows are charged per node that performs the outcall, and the figures assume all `n` of them do and each consumes the same amount, as a fully replicated call is priced: a non-replicated call is charged them once, and a flexible call `total_requests` times. Which term dominates depends on the call: round-trip time is capped at 60 seconds, which is 234 million cycles on a 13-node subnet, while a transform that uses the full instruction limit costs about 5 billion and delivering a 2MB response about 19 billion. **What to attach.** `ic0.cost_http_request_v2` does not return the figure above. Neither how many nodes will respond nor which result they will produce is known when the call is made, and delivering the result has to be paid out of the per-node budgets, so the amount it returns reserves for the most expensive result the call could still produce. It therefore exceeds what the call settles at, and the difference is refunded.