Near Learn

Paginate views, bound every loop

Paginate NEAR view methods with from_index and limit, cap loop sizes, and never iterate an unbounded collection in a change method that can run out of gas.

Intermediate8 min read3-question check

Any loop whose length grows with the number of users will eventually hit a gas limit. It works in testing with ten entries and fails in production with ten thousand. In a view method that means the frontend can no longer load the list; in a change method it can mean a feature, or the funds it manages, is stuck for good.

The rule is simple: every loop has a bound the contract controls, not one that grows with state or that the caller picks freely.

Paginated views#

from_index + limit, with a hard cap and a count
Rust
use near_sdk::store::IterableSet;
use near_sdk::{near, AccountId};

const DEFAULT_LIMIT: u32 = 50;
const MAX_LIMIT: u32 = 100;

#[near(contract_state)]
pub struct Registry {
    members: IterableSet<AccountId>,
}

impl Default for Registry {
    fn default() -> Self {
        Self { members: IterableSet::new(b"m") }
    }
}

#[near]
impl Registry {
    pub fn get_members(&self, from_index: Option<u32>, limit: Option<u32>) -> Vec<AccountId> {
        let from = from_index.unwrap_or(0);
        let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
        self.members
            .iter()
            .skip(from as usize)
            .take(limit as usize)
            .cloned()
            .collect()
    }

    pub fn members_count(&self) -> u32 {
        self.members.len()
    }
}

Bounded work in change methods#

A method like "pay a reward to every holder" cannot be fixed with pagination on the client side, because a single call must finish within its gas. Two designs work. Pull, not push: record what each account is owed (or a global reward-per-share value) and let each user claim their own. Process in batches: keep a cursor in state and let each call handle at most N items, so anyone can call it again until the job is done.

A cursor makes a long job resumable in fixed-size steps
Rust
use near_sdk::store::{LookupMap, Vector};
use near_sdk::{near, AccountId, PanicOnDefault};

const REWARD: u128 = 1_000;
const MAX_BATCH: u32 = 50;

#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Rewards {
    queue: Vector<AccountId>,
    rewards: LookupMap<AccountId, u128>,
    cursor: u32,
}

#[near]
impl Rewards {
    /// Processes up to max_items queued accounts; returns how many were done.
    pub fn process_batch(&mut self, max_items: u32) -> u32 {
        let end = self
            .cursor
            .saturating_add(max_items.min(MAX_BATCH))
            .min(self.queue.len());
        for i in self.cursor..end {
            if let Some(account) = self.queue.get(i) {
                let current = self.rewards.get(account).copied().unwrap_or(0);
                self.rewards.insert(account.clone(), current + REWARD);
            }
        }
        let done = end - self.cursor;
        self.cursor = end;
        done
    }
}

Check yourself

3 questions · progress saved in this browser

  1. 1.View calls are free for the caller. Why should view methods still paginate?
  2. 2.You need to pay a reward to every token holder. Which design scales?
  3. 3.A caller passes limit: 1_000_000 to your paginated view. What should the contract do?