C# Stack vs Queue: LIFO and FIFO Collections Explained
A Stack gives you back what you put in last. A Queue gives you back what you put in first. The order you retrieve things is the entire point of both.
Picture two everyday situations. First: a stack of plates in a cupboard — you always take the top plate, the one most recently placed there, and new clean plates go on top too. Second: a line of people waiting at a coffee shop counter — the first person to join the line is the first one served, no matter how long the line gets. Both are "add things, then take them out later" collections. What differs completely is which item comes out.
In this lesson, you'll learn about Stack<T> and Queue<T> — two collections defined entirely by their retrieval order, and where each shows up constantly in real software.
What Is It?
The Simple Explanation
A Stack<T> is Last In, First Out (LIFO) — the most recently added item is always the first one you get back, like that pile of plates. A Queue<T> is First In, First Out (FIFO) — the earliest added item is always the first one you get back, like that coffee shop line.
The Technical Definition
Both Stack<T> and Queue<T> are generic collections in System.Collections.Generic that deliberately restrict how you access their elements — unlike List<T>, neither lets you index into an arbitrary position. Instead, each exposes only the operations needed to add one item and retrieve/remove the "next" item, where "next" is defined by their opposite ordering rules.
| Operation | Stack<T> (LIFO) | Queue<T> (FIFO) |
|---|---|---|
| Add an item | Push(item) | Enqueue(item) |
| Remove and return the "next" item | Pop() — most recently pushed | Dequeue() — earliest enqueued |
| Look without removing | Peek() | Peek() |
| Check if empty first (safe) | TryPop(out T) | TryDequeue(out T) |
Why Does It Exist?
The Problem
Suppose you're building an "undo" feature for a text editor. Every action the user takes needs to be reversible, and — critically — undoing must always reverse the most recent action first. You could try to manage this with a List<T>:
List<string> actions = [];
actions.Add("Typed 'Hello'");
actions.Add("Typed ' World'");
// To undo, you'd have to remember to always grab the LAST element yourself:
string lastAction = actions[actions.Count - 1]; // easy to get this index wrong
actions.RemoveAt(actions.Count - 1);This works, but nothing about a List<T> stops you from accidentally reading actions[0] instead, undoing the first action ever taken instead of the most recent one. The "must always be the last one" rule lives only in the programmer's head, not in the type itself.
The Solution
Stack<T> bakes that rule directly into the type — there's no index to get wrong, because there's no indexer at all:
Stack<string> actions = [];
actions.Push("Typed 'Hello'");
actions.Push("Typed ' World'");
string lastAction = actions.Pop(); // always the most recent — guaranteed by the type itselfSimilarly, a background job processor needs to handle print jobs in the exact order they were submitted — the first job in line should print first, no exceptions. Queue<T> makes that guarantee structurally, the same way Stack<T> guarantees "most recent first."
Big Picture
Push B → [A, B]
Push C → [A, B, C]
Pop() → returns C (last pushed)
Enqueue B → [A, B]
Enqueue C → [A, B, C]
Dequeue() → returns A (first enqueued)
How It Works
Stack<string> history = [];
history.Push("Page A");
history.Push("Page B");
history.Push("Page C");
string top = history.Peek(); // "Page C" — still there, just looked at
string popped = history.Pop(); // "Page C" — removed and returned
Peekis read-only;Popremoves as it returns.
if (history.TryPop(out string? previous))
Console.WriteLine($"Going back to {previous}");
else
Console.WriteLine("No history to go back to.");
- Calling
Pop()orPeek()on an empty stack throwsInvalidOperationException—TryPopavoids that entirely.
Queue<string> printJobs = [];
printJobs.Enqueue("Report.pdf");
printJobs.Enqueue("Invoice.pdf");
printJobs.Enqueue("Photo.png");
string next = printJobs.Peek(); // "Report.pdf" — the earliest one, still there
string printed = printJobs.Dequeue(); // "Report.pdf" — removed and returned
while (printJobs.Count > 0)
{
string job = printJobs.Dequeue();
Console.WriteLine($"Printing {job}...");
}
- This "drain the queue" pattern — check
Count,Dequeue, repeat — is the standard way to process every item in submission order.
Simple Example
// Stack: an editor's undo history
Stack<string> undoHistory = [];
undoHistory.Push("Typed 'Hello'");
undoHistory.Push("Typed ' World'");
undoHistory.Push("Deleted 'World'");
Console.WriteLine(undoHistory.Pop()); // "Deleted 'World'" — most recent action, undone first
Console.WriteLine(undoHistory.Pop()); // "Typed ' World'"
// Queue: a coffee shop's order line
Queue<string> orderLine = [];
orderLine.Enqueue("Amy's order");
orderLine.Enqueue("Ben's order");
orderLine.Enqueue("Cara's order");
Console.WriteLine(orderLine.Dequeue()); // "Amy's order" — first in line, served first
Console.WriteLine(orderLine.Dequeue()); // "Ben's order"Code → Meaning → Result:
- The stack hands back the most recently pushed action first — exactly the "undo the last thing" behavior we needed.
- The queue hands back the earliest enqueued order first — exactly the "first come, first served" behavior a real line has.
Real-World Example
A background service processes print jobs submitted by many office computers, always in the order they arrived — a perfect fit for Queue<T>. Meanwhile, a document editor built alongside it needs an undo stack — a perfect fit for Stack<T>.
public record PrintJob(string FileName, string RequestedBy);
public class PrintSpooler
{
private readonly Queue<PrintJob> _jobs = [];
public void Submit(PrintJob job)
{
_jobs.Enqueue(job);
Console.WriteLine($"Queued: {job.FileName} (from {job.RequestedBy})");
}
public void ProcessNext()
{
if (_jobs.TryDequeue(out PrintJob? job))
Console.WriteLine($"Printing: {job.FileName} for {job.RequestedBy}");
else
Console.WriteLine("No jobs waiting.");
}
public int PendingJobs => _jobs.Count;
}
var spooler = new PrintSpooler();
spooler.Submit(new PrintJob("Q3-Report.pdf", "Amy"));
spooler.Submit(new PrintJob("Invoice-4821.pdf", "Ben"));
spooler.ProcessNext(); // prints Amy's job — it was submitted first
spooler.ProcessNext(); // prints Ben's job next
// Queued: Q3-Report.pdf (from Amy)
// Queued: Invoice-4821.pdf (from Ben)
// Printing: Q3-Report.pdf for Amy
// Printing: Invoice-4821.pdf for Benpublic class UndoManager
{
private readonly Stack<string> _actions = [];
public void RecordAction(string description)
{
_actions.Push(description);
Console.WriteLine($"Did: {description}");
}
public void Undo()
{
if (_actions.TryPop(out string? lastAction))
Console.WriteLine($"Undoing: {lastAction}");
else
Console.WriteLine("Nothing to undo.");
}
}
var editor = new UndoManager();
editor.RecordAction("Typed 'Hello World'");
editor.RecordAction("Applied bold formatting");
editor.Undo(); // undoes the formatting — the MOST RECENT action
editor.Undo(); // undoes the typing
// Did: Typed 'Hello World'
// Did: Applied bold formatting
// Undoing: Applied bold formatting
// Undoing: Typed 'Hello World'Analogy
A cafeteria's plate dispenser is a physical stack: the spring pushes the most recently added plate to the top, and that's the one every customer grabs. Nobody can reach in and pull a plate from the bottom without disturbing everything above it — the mechanism itself enforces "last in, first out."
A checkout line at a grocery store is a physical queue: whoever joined the line first gets served first, no matter how long the line grows behind them. Cutting to the front is the social equivalent of trying to Dequeue from the wrong end — the structure of a real line simply doesn't allow it.
Under the Hood
- Just like
List<T>, bothStack<T>andQueue<T>use a resizable backing array internally, growing it the same way when it fills up. - What differs is not the storage — it's which operations the type exposes, and from which end of the data they operate.
Pushadds to what's conceptually the "top" of the backing array, andPopremoves from that exact same end.- Because both operations happen at the same end, neither requires shifting any other elements — both are fast, O(1) operations.
Enqueueadds to the "back" of the queue;Dequeueremoves from the "front" — two different ends of the same underlying structure.- .NET's
Queue<T>implementation uses a clever technique called a circular buffer: instead of physically shifting every element forward each time something is dequeued, it just moves an internal "front" pointer forward, wrapping back around to the start of the array once it reaches the end. This keeps bothEnqueueandDequeueO(1), without the cost of shifting elements.
- Neither type exposes an indexer like
stack[2]orqueue[2]— this is intentional. Restricting access to only the "next" item is precisely what makes the LIFO/FIFO guarantee bulletproof; there's no way to accidentally reach around it.
Common Confusion
1. Which method name goes with which type
It's genuinely easy to mix up Push/Pop (Stack) with Enqueue/Dequeue (Queue), especially early on. A memory trick: a stack is something you physically push down on and pop off of (plates, a stack of papers). A queue is a British word for a waiting line, so people queue up (enqueue) and eventually get dequeued from the front.
2. "It's just a List with different method names" — not quite
The restricted API isn't just a style choice — it's the whole point. A List<T> lets you insert or remove anywhere, which means nothing in the type itself guarantees LIFO or FIFO order; a careless RemoveAt(0) on what was meant to be a stack silently breaks the LIFO guarantee. Stack<T> and Queue<T> make that mistake impossible by never exposing the operations that could cause it.
Common Mistakes
Mistake 1 — Calling Pop/Dequeue on an empty collection
Wrong — throws InvalidOperationException:
Stack<int> empty = [];
int value = empty.Pop(); // throws — nothing to pop Correct — check first, or use the Try variant:
if (empty.TryPop(out int result))
Console.WriteLine(result);
else
Console.WriteLine("Stack is empty.");Mistake 2 — Using a Stack when you actually need FIFO order (or vice versa)
Using Stack<T> for a print queue would print the most recently submitted job first, leaving earlier jobs waiting indefinitely if new ones keep arriving — the opposite of "first come, first served." Match the type to the ordering guarantee the problem actually needs.
Mistake 3 — Expecting indexed access
Wrong — neither type has an indexer:
Stack<int> s = [1, 2, 3];
int middle = s[1]; // compiler error If you need to inspect arbitrary positions, you likely want List<T> instead — or you can enumerate a stack/queue with foreach for read-only, order-respecting inspection.
When Should I Use It?
Use Stack<T> when
- You need "undo" behavior — reverse the most recent action first.
- You're tracking navigation history ("back" button behavior).
- You're processing nested structures where the innermost thing must resolve first (matching brackets, backtracking algorithms).
Use Queue<T> when
- Work must be processed strictly in the order it arrived (print jobs, task queues, message processing).
- You're modeling any real "waiting line" — support tickets, order fulfillment.
- You need breadth-first processing of a set of pending items.
Stack<T>. If "first come, first served" describes it, reach for Queue<T>. If neither ordering rule matters and you just need general storage, List<T> is still the better default.
Mental Model
Push/Pop — a pile of platesQueue<T> = FIFO —
Enqueue/Dequeue — a checkout lineRemember:
· Both wrap a resizable array, just like
List<T> — the restriction is deliberate, not a limitation.· Neither has an indexer — you can only ever reach the "next" item.
· Use
TryPop/TryDequeue to avoid exceptions on an empty collection.· Choosing the wrong one silently flips your processing order — match the type to what the problem actually needs.
Key Takeaway
- Stack<T> is Last In, First Out —
Pushto add,Popto remove the most recently added item. - Queue<T> is First In, First Out —
Enqueueto add,Dequeueto remove the earliest added item. - Both are built on a resizable backing array, like
List<T>, but deliberately restrict access to only the "next" item to guarantee correct ordering. - Use
TryPop/TryDequeueinstead ofPop/Dequeuewhenever the collection might be empty. - Match the type to the ordering your problem needs: "most recent first" → Stack; "first come, first served" → Queue.
Check Your Understanding
You've seen how Stack and Queue guarantee opposite retrieval orders. Let's check your understanding.
1. After running Push("A"); Push("B"); Push("C"); on a Stack<string>, what does the first Pop() return?
Show answer
Correct: C
Why C is correct: A stack is Last In, First Out. "C" was pushed most recently, so it's the first one returned by Pop().
Why A is incorrect: "A" was pushed first, which means in a LIFO structure it comes out last, not first.
Why B is incorrect: "B" is in the middle — it will come out on the second Pop(), after "C".
Why D is incorrect: Stack order is fully deterministic and guaranteed — that predictability is the entire reason the type exists.
Reinforcement: LIFO means the most recently added item always comes out first.
2. After running Enqueue("A"); Enqueue("B"); Enqueue("C"); on a Queue<string>, what does the first Dequeue() return?
Show answer
Correct: A
Why A is correct: A queue is First In, First Out. "A" was enqueued first, so it's the first one returned by Dequeue().
Why B is incorrect: "B" was enqueued second, so it comes out on the second Dequeue() call, after "A".
Why C is incorrect: "C" was enqueued last, which in a FIFO structure means it comes out last, not first.
Why D is incorrect: Queue order is based entirely on arrival order — it has nothing to do with the value's content.
Reinforcement: FIFO means the earliest added item always comes out first — the opposite of a stack.
3. A support ticketing system must resolve tickets in the exact order customers submitted them. Which collection is the correct choice, and why?
Show answer
Correct: B
Why B is correct: "Resolve in the order submitted" is a textbook FIFO requirement — the earliest ticket in should be the earliest one out, which is exactly what Queue<T> guarantees.
Why A is incorrect: Stack<T> would resolve the most recently submitted ticket first, leaving older tickets waiting indefinitely if new ones keep arriving — the opposite of what's needed.
Why C is incorrect: The two types produce opposite processing orders — the choice directly determines which tickets get handled first.
Why D is incorrect: A HashSet<T> would guarantee uniqueness, not ordering — it doesn't address the "process in submission order" requirement at all, and it has no Dequeue-style operation.
Reinforcement: "In the order received" is the signature phrase that points to a queue, every time.
4. Why do neither Stack<T> nor Queue<T> provide an indexer like list[2]?
Show answer
Correct: B
Why B is correct: The restricted API is a deliberate design choice, not an oversight. If arbitrary indexing were allowed, code could reach into the middle of a "stack" or "queue" and violate the very ordering guarantee the type exists to provide.
Why A is incorrect: This is intentional design, reinforced consistently across the whole class — not a missing feature.
Why C is incorrect: There's no such size-based restriction anywhere in .NET's collection design.
Why D is incorrect: Neither type exposes any way to read or write an arbitrary position — only Peek (read the next item) and Pop/Dequeue (remove the next item) are available.
Reinforcement: A narrower API is sometimes the feature, not a limitation — it makes incorrect usage impossible to express.
You now know five concrete collection types and when each earns its place. Next, you'll zoom out and see the interfaces — IEnumerable, ICollection, IList, and friends — that tie all of them together.