For AI agents: Documentation index at /llms.txt

Skip to content

Application Architecture

An application on the Internet Computer typically consists of one or more canisters that handle backend logic, store data, and optionally serve a web frontend: all without external servers, databases, or CDNs. This page explains how these pieces fit together and what architectural patterns are available as your application grows.

Most ICP applications start with two canisters:

  • Backend canister: contains your application logic and data. You write it in Motoko or Rust (the official CDKs). Community-supported languages like TypeScript and Python are also available: see Languages. Your code is compiled locally to WebAssembly and executed by the network.
  • Frontend (asset) canister: serves your web UI. It is a standard canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP.

When a user opens your application in a browser:

  1. The browser sends an HTTPS request to a boundary node.
  2. The boundary node routes the request to the frontend canister, which returns the HTML and JavaScript.
  3. The JavaScript uses an agent library (like @icp-sdk/core/agent) to send messages to the backend canister.
  4. The backend canister processes the message, updates its state if needed, and returns a response.
  5. The frontend renders the result.

This flow replaces the traditional web stack. There is no separate web server, application server, or database. The backend canister handles all three roles, and the frontend canister replaces your CDN.

How ICP compares to traditional architectures

Section titled “How ICP compares to traditional architectures”
ConcernTraditional web appICP application
ComputeApplication server (Node, Django, etc.)Backend canister (Wasm)
StorageDatabase (Postgres, MongoDB, etc.)Canister stable memory (up to 500 GiB)
Frontend hostingCDN + static file serverAsset canister
AuthenticationOAuth provider or custom authInternet Identity (passkey-based)
Scheduled tasksCron jobs, worker queuesCanister timers
External API callsServer-side HTTP requestsHTTPS outcalls
Infrastructure managementYou manage servers, scaling, uptimeThe network handles replication and availability

The key difference: ICP applications are self-contained. You deploy code and data to canisters, and the network provides compute, storage, and serving. There is no infrastructure to provision or maintain.

As your application grows, you can choose from several patterns. Start simple and evolve as needed: over-architecting from the start is a common mistake.

Everything (assets, logic, and data) lives in one canister. This is the simplest architecture and works well for applications serving up to thousands of users.

When to use: recommended for most applications. A single canister provides atomic operations and minimal maintenance overhead (no cycle management across canisters, no inter-canister call complexity). Consider multi-canister only when you need separation of concerns or hit a single canister’s platform limits.

Separate canisters handle distinct responsibilities. The two-canister setup (frontend + backend) is the simplest form. You can add more canisters as responsibilities grow: one for user data, one for content, one for payments.

When to use: when you need separation of concerns between components or hit a single canister’s platform limits (memory, compute, or storage).

Things to know:

  • Inter-canister calls are asynchronous. Code before and after an await executes in separate message rounds: this affects atomicity.
  • Request and response payloads are limited to 2 MiB per call.
  • Cross-subnet calls add one consensus round of latency compared to same-subnet calls.

For implementation details and common pitfalls, see Inter-canister calls.

For maximum throughput, distribute canisters across multiple subnets. Each subnet processes messages independently, so spreading load across subnets lets your application scale horizontally.

When to use: high-throughput applications that exceed what a single subnet can handle (thousands of concurrent users, heavy computation).

Trade-offs: cross-subnet calls have higher latency and bandwidth limits. You need to design data partitioning carefully.

Canisters store data in heap memory during execution and can persist data across upgrades using stable memory: there is no external database. Libraries provide familiar data-structure abstractions on top of raw stable memory:

For small to medium datasets, stable memory is straightforward. For applications with large data volumes (hundreds of GiB), see the canister-per-service or canister-per-subnet patterns to distribute storage across canisters.

Not every ICP application needs the default asset canister. Your options:

  • Asset canister: the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to an asset canister that serves it over HTTP. See Asset canister.
  • Framework-specific canister: use a framework like Juno that provides a more opinionated hosting solution on ICP.
  • Offchain frontend: host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using @icp-sdk/core/agent. Useful during migration or when you need features that asset canisters don’t support.
  • No frontend: backend-only canisters that expose a Candid API for other canisters or CLI tools to call.
QuestionIf yesIf no
Start hereSingle canister: recommended for most applications-
Does the app have a web UI?Add an asset canisterBackend-only canister
Do you need separation of concerns or hit platform limits?Canister-per-serviceStay with a single canister
Do you need to scale beyond one subnet?Canister-per-subnetStay on one subnet

Start with the simplest architecture that meets your requirements. You can always split a canister into multiple canisters later: it is much harder to merge canisters that were split prematurely.