The one thing to understand first
PostgreSQL’s C code calls palloc() thousands of times to build a query’s plan, evaluate expressions, and format results — and almost never calls the matching free. That sounds like a leak factory. It isn’t, because every allocation belongs to a memory context: a named arena that can be wiped out in one stroke. When a query ends, PostgreSQL doesn’t free a thousand objects individually — it deletes the context and every allocation inside it vanishes at once. Grasp this and a whole category of “where did my RAM go” questions becomes answerable.
palloc, and the context behind it
The interface lives in src/backend/utils/mmgr/mcxt.c. palloc(size) doesn’t take a context argument — it allocates from the global CurrentMemoryContext. Code sets that target with the standard idiom oldcontext = MemoryContextSwitchTo(ctx); ... ; MemoryContextSwitchTo(oldcontext);. Because allocation is tied to where CurrentMemoryContext points, the same palloc call can land in a short-lived per-tuple arena or a long-lived cache depending on the surrounding switch. pfree exists, but is the exception; the rule is to reset or delete the whole context later.
The allocator: aset.c
The default context type is the allocation set in src/backend/utils/mmgr/aset.c, created by AllocSetContextCreate. It grabs OS memory in blocks that grow geometrically from initBlockSize up to maxBlockSize, and carves each block into chunks for individual pallocs. Freed chunks go onto one of 11 power-of-two freelists (8, 16, 32, … up to 8 KB), so a later same-sized request is recycled without touching the OS. Allocations larger than the chunk limit (ALLOC_CHUNK_LIMIT, ~8 KB) get their own dedicated block. This design makes palloc far cheaper than raw malloc and makes bulk reset essentially free.