Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .sources/examples
Submodule examples updated 2454 files
6 changes: 3 additions & 3 deletions docs/concepts/https-outcalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,17 @@ The cost depends on two factors:
- **Request size**: the combined byte length of the URL, headers, body, transform function name, and transform context.
- **`max_response_bytes`**: the maximum response size you declare. This is what you're charged for, not the actual response size.

If you omit `max_response_bytes`, the system assumes the maximum of 2 MB and charges accordingly: roughly 21.5 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.
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.

For exact pricing formulas, see the [cycles costs reference](../references/cycle-costs.md).

## Limitations

- **HTTPS only.** Plain HTTP is not supported. The target server must have a valid TLS certificate.
- **2 MB response limit.** The maximum response body is 2,097,152 bytes. If the response exceeds `max_response_bytes`, the call fails.
- **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.
- **Public endpoints only.** Canisters cannot reach localhost, private IP ranges (10.x.x.x, 192.168.x.x), or other non-routable addresses.
- **No streaming or WebSocket.** Outcalls are single request-response pairs. Long-lived connections are not supported.
- **~30-second timeout.** If the external server doesn't respond in time, the call fails.
- **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.
- **Rate limiting.** All canisters on a subnet share the same IPv6 prefixes. If many canisters on the same subnet call the same server, they share its rate limit quota. Using API keys with per-key quotas mitigates this.
- **Shared API keys are visible to all replicas.** An API key stored in canister state is readable by every replica. A compromised replica could use the key to make entirely different, unauthorized requests to the external service: not just replay the canister's intended request. [TEE-enabled subnets](node-infrastructure.md#trusted-execution-environments) mitigate this by running replicas in hardware-enforced enclaves, preventing node operators from reading canister memory. Consider deploying canisters that store sensitive credentials on a TEE-enabled subnet.

Expand Down
24 changes: 13 additions & 11 deletions docs/guides/backends/https-outcalls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ For how the consensus mechanism works for outcalls, see [Concepts: HTTPS Outcall

By default, every replica node in the subnet independently makes the same HTTP request: called **replicated mode**. All nodes must agree on the response before execution continues. Two constraints apply regardless of mode:

- [Cycles](../../concepts/cycles.md) to cover the request cost **must be attached** at call time. In Rust, `ic_cdk::management_canister::http_request` auto-calculates and attaches cycles. In Motoko, cycles must be attached explicitly with `await (with cycles = ...)`.
- The **maximum response body is 2MB** (2,097,152 bytes). Requests exceeding this limit fail. Always set `max_response_bytes` to a tight upper bound: omitting it defaults to 2MB and charges cycles accordingly.
- [Cycles](../../concepts/cycles.md) to cover the request cost **must be attached** at call time. Both languages provide a wrapper that computes the exact amount and attaches it: `ic_cdk::management_canister::http_request` in Rust, and `Call.httpRequest` from the `ic` package in Motoko. Prefer these over a hand-picked figure: attached cycles are held for the duration of the call, so an arbitrary margin caps how many outcalls the canister can have in flight.
- The **maximum response size is 2MB** (2,000,000 bytes). This covers the response's header names and values plus the body, so size a cap against both: a response can carry 1–2 KB of headers before any body. Requests exceeding this limit fail. Always set `max_response_bytes` to a tight upper bound: omitting it defaults to 2MB and charges cycles accordingly.

In replicated mode, a transform function is strongly recommended: without one, responses across nodes will likely differ and consensus will fail. In non-replicated mode (`is_replicated = false`), a transform is unnecessary because only one node makes the request. See [Replicated vs non-replicated mode](#replicated-vs-non-replicated-mode) below.

Expand Down Expand Up @@ -51,7 +51,7 @@ A minimal example that sends a GET request to an echo service. The response body

<CodeExample example="send_http_get" lang="motoko">

```motoko snippet="send_http_get/src/send_http_get_backend/main.mo#get_request"
```motoko snippet="send_http_get/backend/main.mo#get_request"
```

</CodeExample>
Expand All @@ -61,7 +61,7 @@ A minimal example that sends a GET request to an echo service. The response body

<CodeExample example="send_http_get" lang="rust">

```rust snippet="send_http_get/src/send_http_get_backend/src/lib.rs#get_request"
```rust snippet="send_http_get/backend/src/lib.rs#get_request"
```

</CodeExample>
Expand All @@ -76,7 +76,7 @@ Because these examples use replicated mode, they include a transform function to

<CodeExample example="send_http_get" lang="motoko">

```motoko snippet="send_http_get/src/send_http_get_backend/main.mo#transform"
```motoko snippet="send_http_get/backend/main.mo#transform"
```

</CodeExample>
Expand All @@ -86,7 +86,7 @@ Because these examples use replicated mode, they include a transform function to

<CodeExample example="send_http_get" lang="rust">

```rust snippet="send_http_get/src/send_http_get_backend/src/lib.rs#transform"
```rust snippet="send_http_get/backend/src/lib.rs#transform"
```

</CodeExample>
Expand All @@ -106,7 +106,7 @@ POST requests work the same way, with two additional considerations:

<CodeExample example="send_http_post" lang="motoko">

```motoko snippet="send_http_post/src/send_http_post_backend/main.mo#post_request"
```motoko snippet="send_http_post/backend/main.mo#post_request"
```

</CodeExample>
Expand All @@ -116,7 +116,7 @@ POST requests work the same way, with two additional considerations:

<CodeExample example="send_http_post" lang="rust">

```rust snippet="send_http_post/src/send_http_post_backend/src/lib.rs#post_request"
```rust snippet="send_http_post/backend/src/lib.rs#post_request"
```

</CodeExample>
Expand All @@ -133,13 +133,15 @@ In replicated mode, a transform function is strongly recommended (without one, r

If the response body also contains dynamic fields (timestamps, per-request IDs, the caller's IP), parse and re-serialize the body to extract only the deterministic fields you need.

`max_response_bytes` is enforced twice: once on the raw response as it arrives from the server, and again on the transform's own output. Stripping headers in the transform therefore cannot rescue a response that already exceeded the cap, because the first check runs before the transform does. It only keeps the transform's own output within the cap. Size `max_response_bytes` for the headers and body as they arrive from the server.

**Debugging "no consensus" errors:** If you see `"No consensus could be reached"`, the transform is not making responses identical. Common culprits: response headers differ, JSON fields arrive in a different order, or the response body contains timestamps. Strip all headers first; if that doesn't resolve it, also normalize or strip the body.

## Cycle costs

HTTPS outcall costs are based on `max_response_bytes`, not the actual response size. If you omit `max_response_bytes`, the system assumes 2MB and charges approximately **21.5 billion cycles**: even for a 1KB response. Always set a tight upper bound. Unused cycles are refunded, but you still pay for the declared maximum.
HTTPS outcall costs are based on `max_response_bytes`, not the actual response size. If you omit `max_response_bytes`, the system assumes 2MB and charges approximately **20.85 billion cycles**: even for a 1KB response. Always set a tight upper bound. Unused cycles are refunded, but you still pay for the declared maximum.

In Rust, `ic_cdk::management_canister::http_request` computes and attaches the exact cost automatically using the `ic0.cost_http_request` system API. In Motoko, cycles must be attached explicitly with `await (with cycles = ...)`.
In Rust, `ic_cdk::management_canister::http_request` computes and attaches the exact cost automatically using the `ic0.cost_http_request` system API. In Motoko, `Call.httpRequest` from the `ic` package does the same. 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.

For reference, on a 13-node subnet:
- Base cost: ~49 million cycles
Expand All @@ -152,7 +154,7 @@ See [Cycles costs](../../references/cycle-costs.md#https-outcalls) for the full

- **Public endpoints only.** HTTPS outcalls can only reach public internet endpoints. Localhost (`127.0.0.1`), private IP ranges (`10.x.x.x`, `192.168.x.x`), and other non-routable addresses are blocked.
- **`Host` header may be required.** Some API endpoints require the `Host` header to be explicitly set. The IC does not automatically set it from the URL: add it to your headers if the server requires it.
- **~30-second timeout.** If the external server does not respond within the timeout, the call traps. Design for failure and handle errors gracefully.
- **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.

## Testing locally

Expand Down
2 changes: 1 addition & 1 deletion docs/references/cycle-costs.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ base_fee = (3_000_000 + 60_000 * n) * n
size_fee = (400 * request_bytes + 800 * max_response_bytes) * n
```

`request_bytes` is the total serialized request size (URL + headers + body + transform name/context). `max_response_bytes` defaults to 2 MiB if not explicitly set by the canister.
`request_bytes` is the total serialized request size (URL + headers + body + transform name/context). `max_response_bytes` defaults to 2 MB (2,000,000 bytes, decimal) if not explicitly set by the canister, which on a 13-node subnet costs roughly 20.85 billion cycles.

| Component | 13-node cycles | ~USD | 34-node cycles | ~USD |
|-----------|----------------|------|----------------|------|
Expand Down
Loading