Skip to content

Commit 707596d

Browse files
mraszykeichhorlclaude
authored
feat: Flexible canister HTTP outcalls and pay-as-you-go pricing (#254)
## Summary Documents the two new HTTPS outcall features: `flexible_http_request` and pay-as-you-go pricing (version `2`). **Interface specification** - New `flexible_http_request` management canister method. A committee of nodes return their individual HTTP responses instead of the subnet reaching consensus on one. Takes an optional `replication` argument (`min_responses`, `max_responses`, `total_requests`), and returns either the responses or a structured error carrying a global error code and per-node resource reports. - New optional `pricing_version` field on `http_request` (`1` or `2`). Version `1` is now deprecated; version `2` prices the resources a call consumes rather than `max_response_bytes`. - New `ic0.cost_http_request_v2` and `ic0.subnet_self_node_count` System APIs. - Non-replicated outcalls are no longer described as experimental. - `public/references/ic.did` and the interface-spec changelog updated to match. **Concepts, guide and reference** - `concepts/https-outcalls.md`: the three outcall modes, and what each pricing version charges for. - `guides/backends/https-outcalls.mdx`: a mode comparison table, a flexible request section with code, and a cycle costs section covering both versions and the `with_expected_*` reservation setters. - `references/cycle-costs.md`: the version `2` formulas, plus a separate "What to attach" section. - `guides/security/https-outcalls.md` and `dos-prevention.md`: flexible mode's weaker integrity guarantee. **Pins** - `ic-cdk-management-canister` added to `upstream.json` as a second `dfinity/cdk-rs` entry, pinned at 0.2.0. `ic-cdk` bumped to 0.20.3. - `.sources/examples` repinned to `1a9a0249`, which carries the Rust outcall examples migrated to the 0.2 builder and the new `send_http_flexible` example (dfinity/examples#1485). ## Structural decisions - **`cycle-costs.md` separates what a call is charged from what to attach.** `ic0.cost_http_request_v2` deliberately quotes more than a call settles at: it floors the response size at `MAX_CANISTER_HTTP_REJECT_BYTES`, scales the fully-replicated consensus fee, and reserves for the most expensive result the call could still produce. Publishing the settle formula as the amount to attach would under-fund calls, so the two are documented separately. The charged formulas are validated against the upstream pricing unit tests. - **`ic-cdk-management-canister` gets its own watched entry** rather than riding on `ic-cdk`'s pin. It versions independently within the same repo, and the `name` field in `upstream.json` exists for this case (the recipes repo uses it for five entries). `scripts/check-upstream-releases.mjs` derives a distinct slug, issue and label from it. - **The Rust and Motoko tabs show different pricing versions.** The Rust `HttpRequest` builder always selects version `2`; Motoko's `Call.httpRequest` still attaches the version `1` cost. That is a packaging lag rather than a language difference, and the prose says so. The flexible request section is Rust only for the same reason. ## Verification - `npm run build` passes: 210 pages, no errors, with every `snippet=` region resolving at the new examples pin. - The pricing figures quoted in the guide were read from a probe canister calling the replica's own `ic0.cost_http_request_v2`, rather than computed from the published formulas. - Flexible outcalls confirmed live: the feature flag flipped to `Enabled` on 2026-09-02, and both replica versions currently running across all 42 mainnet subnets contain that commit. --------- Co-authored-by: Leo Eichhorn <leo.eichhorn@dfinity.org> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 6a5d040 commit 707596d

15 files changed

Lines changed: 423 additions & 59 deletions

File tree

‎.sources/upstream.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,21 @@
102102
},
103103
{
104104
"repo": "dfinity/cdk-rs",
105-
"pinned": "0.20.2",
105+
"pinned": "0.20.3",
106106
"track": "crate",
107107
"crate": "ic-cdk",
108108
"affects": "`ic-cdk` APIs in Rust code blocks. The repo stopped tagging releases (its newest bare-semver tag is two minors behind the published crate), so the crate version on crates.io is the release identity. Read the sections newer than the pin in https://github.com/dfinity/cdk-rs/blob/master/ic-cdk/CHANGELOG.md, then grep docs/ for the symbols they name. `ic-cdk-timers` and `ic-cdk-executor` version separately; check whether they moved too.",
109109
"reference": "https://docs.rs/ic-cdk/latest/ic_cdk/"
110110
},
111+
{
112+
"repo": "dfinity/cdk-rs",
113+
"name": "ic-cdk-management-canister",
114+
"pinned": "0.2.0",
115+
"track": "crate",
116+
"crate": "ic-cdk-management-canister",
117+
"affects": "The HTTPS outcall APIs in guides/backends/https-outcalls.mdx, concepts/https-outcalls.md and references/cycle-costs.md, and the Rust snippets in the send_http_get, send_http_post and daily_planner examples. This crate versions independently of `ic-cdk` in the same repo, so it gets its own entry: 0.2.0 replaced the free `http_request` with the `HttpRequest` and `FlexibleHttpRequest` builders and moved pricing to version 2. Read the sections newer than the pin in https://github.com/dfinity/cdk-rs/blob/master/ic-cdk-management-canister/CHANGELOG.md.",
118+
"reference": "https://docs.rs/ic-cdk-management-canister/latest/ic_cdk_management_canister/"
119+
},
111120
{
112121
"repo": "dfinity/icp-js-core",
113122
"pinned": "v6.1.0",

‎docs/concepts/https-outcalls.md‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ Canisters on the Internet Computer can make HTTP requests to any public web serv
99

1010
ICP runs every canister on a subnet where all replicas execute the same code independently and must reach consensus. Outbound HTTP requests are non-trivial in this model: each replica independently contacts the server and typically receives a slightly different response: timestamps, headers, or field ordering vary, which would cause replicas to diverge. The traditional workaround is **oracles**: third-party services that fetch external data and relay it to the network, at the cost of extra complexity, fees, and a trust assumption. HTTPS outcalls solve the problem directly: the subnet reaches consensus over the response internally, so canisters call external APIs without a middleman.
1111

12-
## Replicated and non-replicated mode
12+
## Outcall modes
1313

14-
HTTPS outcalls have two modes controlled by the `is_replicated` field:
14+
HTTPS outcalls come in three modes. Two are selected by the `is_replicated` field of the `http_request` method; the third is a separate management canister method, `flexible_http_request`.
1515

1616
**Replicated mode** (default) is what the consensus mechanism below describes: all replicas independently fetch the URL, a transform function normalizes the responses, and the subnet agrees on a single result. This provides the strongest integrity guarantee: the response is confirmed by a supermajority of nodes, making it extremely difficult for any single party to tamper with it. The tradeoff is that all replicas (typically 13) send the same request to the external server within milliseconds of each other, which can trigger API rate limits.
1717

1818
**Non-replicated mode** (`is_replicated = false`) has a single replica make the request. No consensus is needed, so there is no transform function requirement and no rate-limit pressure on the external server. The tradeoff is trust: the single replica that handles the request could theoretically observe or modify the response before returning it to the canister. This mode is appropriate when the endpoint is idempotent, rate limits are a concern, or you're making POST requests where duplicate submissions would cause problems.
1919

20+
**Flexible mode** (`flexible_http_request`) has a committee of nodes make the request and hands the canister their individual responses rather than one agreed result. The canister decides what to make of them. For example: take a median, require that some of them match, or use the first that parses. The caller sizes the committee and states how many responses it needs and is willing to receive. A smaller committee costs less, a larger one is harder for any single node to influence. This suits endpoints whose data changes too fast for replicas to ever agree, such as live prices or feeds that stamp every response, where replicated mode would simply fail to reach consensus. The tradeoff is that reconciling the responses becomes your canister's job.
21+
22+
Flexible outcalls are always priced with pay-as-you-go pricing (version 2), described under [Cycle costs](#cycle-costs) below.
23+
2024
## How outcalls reach consensus
2125

2226
When a canister calls the management canister's `http_request` method, the following happens:
@@ -33,6 +37,8 @@ When a canister calls the management canister's `http_request` method, the follo
3337

3438
The transform function is critical. Without it, even minor differences between responses (a header timestamp off by a millisecond) prevent consensus. If consensus cannot be reached, the call eventually times out: this is the most common failure mode when developing outcalls.
3539

40+
Flexible outcalls follow the same path, with two differences. In step 2 only the committee the caller sized issues the request, not every replica. And in step 5 the subnet agrees on which responses to deliver rather than on what the response says, so responses that disagree are returned instead of failing the call. The transform still runs, on each node's own response.
41+
3642
> **Local testing caveat:** The local replica runs a single node, so all responses pass consensus automatically: even without a transform function. Transform and consensus issues only surface when you deploy to a multi-node subnet.
3743
3844
For practical guidance on writing transform functions, see the [HTTPS outcalls guide](../guides/backends/https-outcalls.md).
@@ -53,7 +59,7 @@ A common pattern is stripping all response headers (they frequently contain time
5359

5460
## Request types and idempotency
5561

56-
HTTPS outcalls support `GET`, `HEAD`, and `POST` methods.
62+
HTTPS outcalls support `GET`, `HEAD`, and `POST` in every mode. `PUT`, `DELETE`, and `PATCH` are restricted to the modes where the number of requests and responses is fixed and known: non-replicated mode, and flexible mode when the committee size and the required and accepted response counts are all equal. The restriction exists because replicated outcalls with `is_replicated = true` do not wait for every request to finish, so one mutating request could land after a later one and undo it.
5763

5864
**GET and HEAD** requests are straightforward: they're inherently idempotent (repeating them doesn't change server state), so having 13 replicas send the same GET is harmless. `HEAD` is particularly useful for determining a resource's response size before making the actual request, which helps you set `max_response_bytes` accurately.
5965

@@ -67,21 +73,33 @@ Not all servers support idempotency keys, so evaluate this on a case-by-case bas
6773

6874
## Cycle costs
6975

70-
HTTPS outcalls are not free. The calling canister must attach cycles to cover the cost. Both the Motoko `ic` mops package and the Rust `ic-cdk` provide wrappers that automatically compute and attach the required amount using the `ic0.cost_http_request` system API.
76+
HTTPS outcalls are not free. The calling canister must attach cycles to cover the cost. The system API reports how many cycles to attach, so a canister never has to hard-code a price: `ic0.cost_http_request_v2` for pay-as-you-go pricing, and the older `ic0.cost_http_request` for deprecated legacy pricing (charged in advance).
77+
78+
There are two pricing models, chosen per call by the `pricing_version` field.
79+
80+
:::caution[Version 1 is deprecated]
81+
82+
Version 1 is still the default, and is what a call gets unless it asks for version 2. It is nonetheless deprecated: version 2 is to become the default, after which version 1 will be removed. New canisters should select version 2, and existing canisters should plan to migrate.
7183

72-
The cost depends on two factors:
84+
:::
85+
86+
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.
87+
88+
**Version 1** charges based on the following two factors:
7389

7490
- **Request size**: the combined byte length of the URL, headers, body, transform function name, and transform context.
7591
- **`max_response_bytes`**: the maximum response size you declare. This is what you're charged for, not the actual response size.
7692

7793
If you omit `max_response_bytes`, the system assumes the maximum of 2 MB and charges accordingly: roughly 20.85 billion cycles on a 13-node subnet. Always set this to a reasonable upper bound for your expected response to avoid overpaying. Unused cycles are refunded.
7894

79-
For exact pricing formulas, see the [cycles costs reference](../references/cycle-costs.md).
95+
**Version 2** charges for what the call actually consumes: the bytes that arrive, the time the request takes, the instructions the transform function runs, and the size of the response that is delivered. `max_response_bytes` still bounds the response, but it no longer sets the price. A generous cap therefore adds nothing to the charge. But because the worst-case usage that bounds the reservation is computed from it, a generous cap holds more cycles for the duration of the call, which limits how many outcalls the canister can have in flight. The tradeoff is that the attached cycles are not only the payment but also the budget the call runs within. A call that does not cover the base fee is rejected up front. Beyond that, attaching less than the call needs is accepted: it runs with proportionally smaller limits on response size, response time, and transform instructions, and fails partway through rather than up front. Use `ic0.cost_http_request_v2` to compute a recommendation of what to attach.
96+
97+
For exact pricing formulas for both versions, see the [cycles costs reference](../references/cycle-costs.md).
8098

8199
## Limitations
82100

83101
- **HTTPS only.** Plain HTTP is not supported. The target server must have a valid TLS certificate.
84-
- **2 MB response limit.** The maximum is 2,000,000 bytes (decimal, not 2^21). The limit covers the response's header names and values plus the body, not the body alone, and it is enforced twice: on the raw response as it arrives from the server, and again on the output of the transform function. A transform therefore cannot rescue a response that already exceeded the cap, because the first check runs before the transform does. Size `max_response_bytes` for the headers and body as they arrive from the server.
102+
- **2 MB response limit.** The maximum is 2,000,000 bytes (decimal, not 2^21). The limit covers the response's header names and values plus the body, not the body alone, and it is enforced twice: on the raw response as it arrives from the server, and again on the output of the transform function. A transform therefore cannot rescue a response that already exceeded the cap, because the first check runs before the transform does. Size `max_response_bytes` for the headers and body as they arrive from the server. In flexible mode the responses delivered together must additionally fit a 2 MiB total.
85103
- **Public endpoints only.** Canisters cannot reach localhost, private IP ranges (10.x.x.x, 192.168.x.x), or other non-routable addresses.
86104
- **No streaming or WebSocket.** Outcalls are single request-response pairs. Long-lived connections are not supported.
87105
- **Two timeouts.** If the external server does not respond within 30 seconds, or the subnet does not produce a response within 60 seconds, the call is rejected. It does not trap, so handle the error case rather than relying on a trap.
@@ -100,12 +118,6 @@ For exact pricing formulas, see the [cycles costs reference](../references/cycle
100118

101119
HTTPS outcalls can replace oracles for most use cases: price feeds, API queries, webhook notifications, and data verification. Oracles may still be useful if you need features like aggregated multi-source data feeds or historical data caching that an oracle provider maintains as a service.
102120

103-
## Future extensions
104-
105-
One extension is under consideration that may affect architecture decisions:
106-
107-
- **Multiple responses:** Instead of consensus on a single response, the canister could receive all individual replica responses and resolve differences in application logic: useful for fast-moving data like price feeds.
108-
109121
## Next steps
110122

111123
- [HTTPS outcalls guide](../guides/backends/https-outcalls.md): practical how-to with code examples in Motoko and Rust

‎docs/concepts/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Understand the ideas behind the Internet Computer before you build on it. These
3333
- **[Orthogonal persistence](orthogonal-persistence.md)**: How canister memory survives across executions and upgrades without databases.
3434
- **[Timers](timers.md)**: Periodic and one-shot scheduled tasks via the global timer mechanism.
3535
- **[Verifiable randomness](verifiable-randomness.md)**: Cryptographically secure random numbers using threshold VRF.
36-
- **[HTTPS outcalls](https-outcalls.md)**: How canisters make HTTP requests to external services with consensus on responses.
36+
- **[HTTPS outcalls](https-outcalls.md)**: How canisters make HTTP requests to external services, with or without consensus on the response.
3737

3838
## Cryptography
3939

0 commit comments

Comments
 (0)