Queues and the order of a help-desk simulation
Model a single FIFO help desk with explicit arrival ties, start times, waiting times, and idle periods, then separate service order from event scheduling.

A first-in, first-out queue answers one question in a help-desk simulation: which waiting request starts service next? It does not determine how long service lasts or which future event happens first. For a developer implementing a basic simulation, separating those decisions prevents a queue from silently turning into a priority scheduler.
If you know Python loops and tuples, assume one agent, no interrupted service, and requests with finite nonnegative arrival times and service durations. Requests arriving at the same time keep their input order. The queue accepts arrivals at the back and removes the oldest waiting request from the front. All times in the example use arbitrary simulation units, not measurements from a real support team.
Work through four requests
Take request A arriving at time zero and needing four units of service. B and C both arrive at time one, needing two and one units respectively. D arrives at time eight and needs two units. A occupies the agent until time four. B then starts, followed by C, because their tie was resolved by input order.
The resulting service intervals are A [0, 4), B [4, 6), C [6, 7), and D [8, 10). The agent is idle from seven to eight. Waiting time is service start minus arrival, giving zero for A, three for B, five for C, and zero for D. The average wait is two units, even though no request waits exactly two.
C requires less service than B but still waits behind it. Choosing C first would implement a different scheduling policy. Shortest-job-first, deadline order, and medical triage can all be reasonable policies in their own settings; they need different rules and should not be described as FIFO.
An event-driven loop without clock ticks
A simulation need not increment time one unit at a time. If the agent is idle, jump directly to the next arrival. After choosing a request, jump to its finish time. Add all arrivals through the current time before choosing the next waiting request.
from collections import deque
def simulate(requests):
jobs = sorted(requests, key=lambda job: job[1])
if any(arrival < 0 or duration < 0 for _, arrival, duration in jobs):
raise ValueError('times must be nonnegative')
waiting, result = deque(), []
next_job, now = 0, 0
while next_job < len(jobs) or waiting:
if not waiting:
now = max(now, jobs[next_job][1])
while next_job < len(jobs) and jobs[next_job][1] <= now:
waiting.append(jobs[next_job])
next_job += 1
name, arrival, duration = waiting.popleft()
start = now
now += duration
result.append((name, start, now, start - arrival))
return result
requests = [('A', 0, 4), ('B', 1, 2), ('C', 1, 1), ('D', 8, 2)]
assert simulate(requests) == [
('A', 0, 4, 0), ('B', 4, 6, 3),
('C', 6, 7, 5), ('D', 8, 10, 0),
]
assert simulate([]) == []
Python’s sort is stable, so equal arrival times retain their input order. The implementation records arrivals that occur during service when it reaches the service completion. This produces the same FIFO order as adding them at their exact arrival moments because service cannot be interrupted and no decision occurs between the start and finish. It would be insufficient for a model that measures queue length continuously or supports cancellation while waiting.
The queue is a deque. Python documents approximately O(1) appends and pops at either end. Removing index zero from an ordinary list shifts the remaining references, which can make a long series of dequeues quadratic. The tutorial’s queue example explains that choice.
State an invariant for the waiting room
Immediately before removing a request, the deque contains every arrived, unserved request in arrival order with the declared tie rule. Earlier service removed only its front. New arrivals append behind existing waiters. Therefore each removal selects the earliest eligible request.
The next_job index separates future arrivals from requests already placed in the queue. The outer condition says work remains if either group is nonempty. When both groups are empty, every request has exactly one result. With zero-duration requests, simulated time may remain unchanged, but removing a request still advances the amount of completed work, so the loop terminates.
For n arbitrary input requests, sorting costs O(n log n). Each request then enters and leaves the deque once, giving O(n) simulation work after sorting. If the input contract guarantees sorted arrivals, you can omit that first sort. The queue can hold O(n) requests when many arrive before the agent finishes a long job; the returned result also uses O(n) space.
Two averages with different denominators
The four recorded waits add to eight time units. Dividing by four requests gives the average wait of two. A time-average queue length asks a different question: if you observe a uniformly chosen instant during [0, 10), how many requests are waiting, excluding the one in service?
From time one through four, both B and C wait, so the queue contributes 2 * 3 = 6 request-time units. From four through six, only C waits, contributing another two. There are no waiting requests during the rest of this observation window. The total area under queue length is eight, and dividing by the ten-unit window gives 0.8 waiting requests on average.
Every request contributes a rectangle of height one over its own waiting interval [arrival, start). Summing the rectangles by request gives total waiting time; slicing the same rectangles along the time axis gives the queue-length area. This is an accounting identity for the fully observed requests. It does not assume a probability distribution or establish a steady-state forecast for future traffic.
from collections import defaultdict
def waiting_area(requests):
jobs = sorted(requests, key=lambda job: job[1])
records = simulate(jobs)
changes = defaultdict(int)
for (_, arrival, _), (_, start, _, _) in zip(jobs, records):
changes[arrival] += 1
changes[start] -= 1
area, count, previous = 0, 0, 0
for time in sorted(changes):
area += count * (time - previous)
count += changes[time]
previous = time
assert count == 0
return area
records = simulate(requests)
assert sum(wait for _, _, _, wait in records) == 8
assert waiting_area(requests) == 8
assert 8 / len(records) == 2
assert 8 / 10 == 0.8
Changes at the same instant are combined before the count is used for the next interval. A request that starts immediately adds and removes itself at that instant, contributing zero area. This also handles zero-duration service: it may create no service interval but can still follow a long wait. The helper sorts interval endpoints to reconstruct a metric; that extra O(n log n) work is separate from the original simulation loop.
Do not estimate queue area by taking the deque’s length only at service completions. The compact simulator postpones adding some arrivals until those completions, so its stored deque is not a continuous record of the physical waiting room. The arrival/start intervals recover the missing history. If an observation window cuts through a wait, clip that interval to the window before computing its area; the complete wait and the clipped contribution are then different quantities.
Try adding E with arrival one and duration zero after C in the input order. It starts at seven and waits six units. The total waiting time rises to fourteen and the mean over five requests becomes 2.8, even though the final completion remains ten. Predict those values before running the simulator. This exercise makes the denominator and the difference between service duration and waiting time visible.
Extend the model where its assumptions stop
Two service agents require a separate structure for finish events or next availability. The earliest finishing agent is chosen by time, while waiting requests can still follow FIFO. A priority queue can manage the agents without changing the waiting-room policy. Mixing both responsibilities into one queue usually obscures which ordering each operation preserves.
Production waiting time also depends on abandoned requests, retries, service categories, and arrival patterns. This deterministic example makes no statistical prediction about any team. To run experiments, supply a stated arrival and service distribution, keep the random seed, and report the quantity being estimated. Even then, the average wait hides the longest waits. Keep the per-request start and finish records so that you can inspect tail behavior rather than inferring it from a single average.