Motoko EOP: Are temporary update-call allocations still billed after garbage collection?

I am trying to understand how temporary heap allocations behave under Motoko’s enhanced orthogonal persistence and incremental garbage collector.

Consider this simplified architecture:

```

import List “mo:core/List”;

import Map “mo:core/Map”;

import Principal “mo:core/Principal”;

persistent actor {

type Record = {

id : Nat;

owner : Principal;

value : Text;

};

type Audit = {

recordId : Nat;

action : Text;

};

// Persistent application state

let records = Map.empty<Nat, Record>();

let recordsByOwner = Map.empty<Principal, List.List>();

let auditsByRecord = Map.empty<Nat, List.List>();

public shared func processRecords() : async Nat {

// A helper or third-party library creates a large temporary

// array/list while processing the persistent maps.

let temporaryWorkingSet =

  SomeLibrary.buildWorkingSet(records);



// Only a small result is returned.

let result = SomeLibrary.summarize(temporaryWorkingSet);



result

};

}

```

Assume:

- `processRecords` is an update call.

- It contains no `await`.

- The temporary working set is never assigned to an actor field.

- No map, list, cache or other object retains it.

- The update completes successfully and returns a small result.

- The temporary allocation causes Wasm memory to grow temporarily from 100 MiB to 1 GiB.

- After the response, only 100 MiB of application data remains logically live.

My expected lifecycle is:

```

Before update:

Live data: approximately 100 MiB

Allocated Wasm memory: approximately 100 MiB

During update:

Live data: approximately 100 MiB

Temporary working set: approximately 924 MiB

Allocated Wasm memory: approximately 1 GiB

After response:

Live data: approximately 100 MiB

Unreachable garbage: approximately 924 MiB

After GC:

Live data: approximately 100 MiB

Reclaimed/reusable heap: approximately 924 MiB

```

I would appreciate clarification on the following:

1. After the successful update, is the unreachable 924 MiB initially committed as part of the canister’s Wasm memory until GC processes it?

2. After a complete incremental GC, does `canister_status.memory_size` decrease toward 100 MiB, or does it normally remain near the 1 GiB high-water mark?

3. If `memory_size` remains near 1 GiB, is the canister charged storage cycles for the complete 1 GiB even though only approximately 100 MiB is logically live?

4. Does GC merely make the reclaimed 924 MiB reusable for future allocations, without returning those Wasm pages to the IC?

5. Are the Wasm instructions executed by Motoko’s incremental GC charged to the canister? If GC work is deferred, is that cost assigned to later update calls?

6. Does `__motoko_gc_trigger()` only advance GC and reclaim internal heap blocks, or can it also reduce the charged `memory_size`?

7. Is there any supported way to shrink an EOP canister’s allocated Wasm memory after an accidental temporary spike?

8. If the update traps instead of succeeding, is the memory growth rolled back while the executed instructions are still charged?

9. Would the behaviour be different for a query call, where temporary heap changes are not committed?

10. Which metrics are recommended for distinguishing live data from allocated high-water memory?

```

rts_heap_size

rts_memory_size

rts_total_allocation

rts_reclaimed

rts_max_live_size

canister_status.memory_size

```

My main question is:

> Under EOP, can one successful update that temporarily allocates 1 GiB permanently raise the canister’s storage billing and reported `memory_size`, even after almost all of that allocation becomes unreachable and is reclaimed by GC?

Relevant documentation:

- [Enhanced orthogonal persistence](https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/orthogonal-persistence/enhanced/)

- [Motoko garbage collector options](https://docs.internetcomputer.org/guides/canister-management/optimization/#motoko-garbage-collector-options)

- [GC recommendations and `__motoko_gc_trigger()`](https://docs.internetcomputer.org/languages/motoko/reference/changelog/#0104-2024-01-10)

- [ICP cycles and storage charging](https://docs.internetcomputer.org/concepts/cycles/)

- [Cycle-cost table](https://docs.internetcomputer.org/references/cycle-costs/)

- [Collecting Garbage on the Blockchain](https://internetcomputer.org/whitepapers/Collecting%20Garbage%20on%20the%20Blockchain.pdf)

This is great thank you for writing this out. I too would like to understand how this works better.

My understanding is that memory and heap grow, but do not shrink. So if you have one update call that expands your memory to some massive number that gets garbage collected away, you’re still gonna end up with the higher amount reserved forever and ever amen.

And why cannot they shrink upon upgrade? Reserved space?

We ran into this and it was nightmarish to solve so we solved for a strategy best practice if you will. You can’t return pages, so don’t take them.

The master rule: don’t rebuild collections. Prefer structures that mutate in place; a rebuild-on-write is a whole collection of garbage for one field.

  1. Bounded logs get a ring buffer (preallocated slots, write + wrap), never grow-then-trim. No container churn per add. You still allocate the element, and the ring’s own size is a fixed floor you pay up front, which is the point: a known floor beats an unbounded one.
  2. Don’t build strings on the happy path. Format only when something actually fired.
  3. Log transitions, not ticks: keep the last value, write only on change.
  4. Skip redundant writes, especially any write that rebuilds a collection.
  5. No O(n) concat trees. Single-pass(ideal)
  6. Never rebuild a whole collection per tick.

log transitions and not ticks

Based sir amazing very nice

To be more clear from our experience the temporaries are collected and the space is reused ie nothing leaks per se

What can’t come back is the page count, because Wasm has memory.grow but no shrink. So the memory returns to the allocator; the meter doesn’t go back down.

That means the cost is driven by your peak memory footprint, not your cumulative allocation. One 500 MB spike can set the floor; another thousand 500 MB spikes don’t raise it further if they reuse the pages already allocated.

Which is why the real danger isn’t allocation churn by itself. It’s a workload that keeps pushing the high-water mark higher instead of reusing the memory it already claimed.

I agree with your assement about workloads and watermarks. Burst mem is a pain but if done correctly can allocate well within responsible limits and not be an issue long term.

The cycles must burn.

                          ░░░▒       ░   ░░ ▒▓▒█▒░                   ░          
                    ▒         ░░       ░▒                    ░░░▒▒▒▒▒▒▓▓▒▒▒░░   
        ░           ▒         ░░░           ░ ░░▒▒▓███▒  ░░▒▓████▓▓▓▓▓▓███▓▒▒▓▒▒
                    ░                  ░▓░   ░▒  ▓▒▒▓░░░▒▒░▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▓▓███▓
                                         ▒▓███████▒▒▒▒▒▒▓██▓▓▒▒▒▓▓▓▓▒▓▓▓▒▒▒▒▓▓██
█████▓██▓▒▒ ░▒▒▒▒░░▓███▒                   ░░░ ▒█▒▓▓▓██▓▓▒▒▒▒▒░▒▓▓▓▓▓▓▓▓▓▓▓▓▓█▓▓
█░░█▓ ██░▒▒        ▓███▓ ▓██▓▓▒░░░░         ░░░░▒▓▓██▓▒▒░░░░ ░░▒▒▓██████████████
█░░██▓██ ▒▒▓▒   ▒█▓▒▒    ▒▒▒▓▒  ░▒█░     ░░  ▓▒▒▓███▓░░       ░▒▓▓██████████████
█░▓▓▓▓█▓   ▓▓░░▓███▓     ▒▒▒▒░   ██▓  ░ ░▒▒▓▓▒░▒███▓░        ░░▒█▓▓█████████████
█          ▓█▒ ████░  ▒▒░▒▒░░   ░███░    ▒▓▒▒▒░▓██▓░         ░▒▓█▓▓▓████████████
█          ▓█  ▓███▒        ░▒░ ░███     ▒▒▒▒▒▓███▒░         ░░▒▓█▓▒▓███████████
    ▓  ░▒▒▒▒▒▒▓████▒▒▒▒▒▓▓▓▓▓▓▒▒▓███▓▓▒▒░▒▓░  ░███▒░          ░░ ░▒▒▒▒██████████
▓▒▒▒██▓░  ░▒░░▒██▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█▓░░░░░░░░░▓█████▓▒▒▒░▒▒▒▓▓█▓▓▒░ ░▒▒██████████
                         ░░░░░░░░▒▓▒░░░░░░░░▒▒████████▒░▒▓███████▓▒▒░▒▓█████████
▒▒▓▒▒▒▒▒▒▒▒▒░               ░░░░░░░▒▒▒▒▒▒░░  ▒██████▓██  ▒▒▓█▒▓█████▒▒▓█████████
 ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒░░▒▒▒▒▒▓▓▓████▓█████░░░░      ░░▒▒░     ▒▓████████
▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓█████                    ░█████████
▒▒▒▒▒▒▒▒▒░░░░░░░░░░▒▒░░░▒░░▒░▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓█████▓                    ▒█████████
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓█████░░░ ░▓█▓▒░░      ░░░▒█████████
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▓▓█▓██▒▒░░░░░░░░▒▒▒▒▒▒▒▒▒▓██▓██████
▒▓▓▓▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▒▒▒▒▓▒▒▒░▒▓█████▓▓▒▒▒▒▒▒▒▓▓████████
▒▓▓▓▒▓▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▒░ ░▒▒▒▒░░░░▒▒▒▒▒▓▓█████████
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓▓▓▓▓▓▒▒▒▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▒      ░░░░▒▒▒▒▓▓▓▓▓██████
▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██▓▓▓▓▓██▓▓▓█████████▓▓▓▓▓▓▓▓▓▓▓▓▓▒░░░░ ░░░░░░▒▒▓▓▓▓▓▓▓▓▓▓██