Most articles about amortized analysis show you the math, declare that Array.prototype.push is amortized O(1), and call it a day. That is technically correct and completely useless if you ever ship code with a latency budget. Amortized O(1) is not worst-case O(1), and conflating the two is the reason your p99 looks fine right up until the moment your service is paged at 3am because a single dynamic-array resize blew through the SLA.
I have spent enough on-call shifts staring at spiky latency graphs to take amortized analysis seriously, and not in the textbook way. This piece is the version of the lecture I wish someone had given me before I shipped my first cache layer.
The intuition: paying ahead for cheap operations
Amortized analysis is the answer to a specific question: given a sequence of operations, what is the average cost per operation? Not average over random inputs, but average over a worst-case sequence. The trick is that some operations might be expensive, but if those expensive ones are rare and the cheap ones are very cheap, the per-operation average can still be small.
The canonical example is the dynamic array. When you push past the current capacity, the array doubles in size and copies every existing element to the new buffer. That single push is O(n). But before that push, you got n cheap pushes that were each O(1). If you spread the cost of the resize across those cheap pushes, each push pays a constant share of the resize. The amortized cost per push is O(1).
Notice the assumption hiding inside that argument. We are talking about the cost averaged over a sequence. Any single push can still be O(n). If your code path cares about the cost of a single operation, the amortized bound is the wrong number to look at.
A worked example that does not lie about the variance
Let me walk through what actually happens. Start with an empty array of capacity 1. We push elements one at a time and double the capacity whenever it fills.
| Push # | Action | Work done | Capacity after |
|---|---|---|---|
| 1 | Insert | 1 | 1 |
| 2 | Resize 1→2, copy 1, insert | 2 | 2 |
| 3 | Resize 2→4, copy 2, insert | 3 | 4 |
| 4 | Insert | 1 | 4 |
| 5 | Resize 4→8, copy 4, insert | 5 | 8 |
| 6,7,8 | Insert (3x) | 3 | 8 |
| 9 | Resize 8→16, copy 8, insert | 9 | 16 |
Total work for 9 pushes: 24 units. Average per push: ~2.7. As you keep pushing, the average converges to about 3 (each element is copied at most twice on average). That constant ratio is the amortized bound. But push #9 alone did 9 units of work. Push #17 will do 17 units. Push #1025 will do 1025 units. The variance is real.
This is the picture I keep in my head when someone says "it is O(1) amortized". Yes, the average is constant. The peak is anything but.
The three accounting methods, plain English version
Classical CS textbooks teach three formal techniques for amortized analysis. They sound scary. They are actually three different ways of saying the same thing.
Aggregate method. Add up the total cost of n operations and divide by n. For dynamic-array push, the total cost of n pushes is at most 3n (each element is copied at most twice on average plus its own insertion). Divide by n to get amortized cost 3 = O(1). This is the method I use 90 percent of the time. It is direct, mechanical, and works for most problems you actually meet.
Banker's method (accounting method). Pretend each operation pays a fixed amount of "credit". Cheap operations bank credit; expensive operations spend it. If the bank is never overdrawn, the fixed payment is your amortized cost. For dynamic-array push, charge each push 3 credits: 1 to do the insert, 2 banked for future copies. When a resize copies n/2 elements, the bank has 2 * (n/2) = n credits ready to pay for them. The bank balances. Amortized O(1).
Potential method. Define a function (the "potential") that measures how much "future cost" is stored in the data structure. The amortized cost of an operation is its actual cost plus the change in potential. Pick the right potential function and the math works out. This is the technique I almost never use except when the other two get awkward (e.g. splay trees).
If you remember only one method, remember the aggregate method. It is the one that maps cleanest to "divide total work by total operations".
When amortized stops being honest
Here is where most resources stop, and where the engineering problems start. Amortized O(1) is genuinely good enough in most situations. It stops being good enough in three specific cases I have actually hit in production.
Real-time deadlines. If you have a 16ms render budget (60fps) or a 1ms cache lookup SLO, you cannot afford the worst-case spike, even if it is rare. The single resize that costs O(n) will violate the deadline on the unlucky frame. In a UI thread, that frame drops. In a payment system, that request gets retried and now you have an idempotency problem.
Predictable latency systems. Networking, embedded, and database internals often need predictable response times more than they need low average response times. Amortized analysis says "the total work over a million calls is bounded". They want to hear "every single call is bounded". Different question, different answer.
Concurrent / contended structures. If the rare expensive operation also takes a global lock (typical for a hash-table resize that has to rehash everything), every other thread blocks on it. Amortized O(1) for the operation that holds the lock translates to O(n) stalls for everyone else waiting in line. The amortized bound is per-thread; the contention is global.
In all three cases, the right answer is to use a structure with a tighter worst-case bound. Persistent / functional data structures, paged hash tables that resize incrementally, deamortized variants that spread the rebalance work across operations explicitly. You give up some average-case performance to flatten the variance.
Two real examples worth remembering
Dynamic arrays are the textbook case, but there are two others I see often.
Hash table resize. A hash table with load factor near 1 will, on insert, rehash every element into a doubled bucket array. That insert is O(n). Spread across n previous cheap inserts, the amortized cost is O(1). Same shape as dynamic-array push. Same caveat: a single insert can spike to O(n), and during the rehash, every other reader and writer either blocks or sees stale data depending on the implementation.
Splay trees. A splay tree rotates the just-accessed element to the root. A single access can do O(n) rotations in the worst case. Over a sequence of m accesses, the total work is O((n + m) log n). Amortized cost per access is O(log n). The variance is higher than a balanced BST, which is why splay trees are rare in production despite their elegant theoretical properties.
The pattern is the same in all three. Cheap-cheap-cheap-EXPENSIVE-cheap-cheap-cheap-EXPENSIVE. Average looks fine. Tail latency looks bad.
When I would ignore everything I just wrote
For most code, amortized O(1) is the answer you want. If you are writing a CRUD endpoint that handles a few thousand requests per minute, the difference between worst-case and amortized cost on your dynamic array is invisible. The GC pause from your runtime will dominate any single-resize cost by an order of magnitude. The network round trip will dominate the GC pause by another order of magnitude. Amortized analysis matters when you have already squeezed out all the easier wins.
The rule I follow is roughly this. If I am writing a normal application service, I trust amortized bounds and move on. If I am writing infrastructure (caches, queues, databases, schedulers), I read the actual source for any "amortized" structure I touch and decide whether the tail latency it introduces is acceptable for my deadline. Most of the time it is. The 5 percent of the time it is not, I have to pick a different structure or pre-allocate to dodge the resize entirely. Amortized analysis is a useful frame, not a permission slip. The honest version of the answer always includes "…but the worst case is O(n) and here is when that bites". If your amortized argument cannot survive that follow-up, the argument is incomplete.
