Senior 3 min · March 24, 2026

Job Sequencing — First-Free-Slot Bug Costs $50k

Assigning jobs to earliest free slot can cause high-profit jobs to miss deadlines, in a $50k penalty.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Sort jobs by profit descending: highest profit first.
  • For each job, find the latest free slot ≤ its deadline.
  • Assign job to that slot — keeping earlier slots open for other jobs.
  • Naive O(n²) slot search; Union-Find reduces to near O(n log n).
  • Proven optimal via exchange argument: any optimal schedule can be transformed to this greedy result without reducing profit.
Plain-English First

You have a list of freelance jobs, each paying different amounts but with a deadline (must be done by day X, one job per day). You want to maximise earnings. The greedy insight: always try to schedule the highest-paying job as late as possible before its deadline — this leaves earlier slots free for other jobs.

Job scheduling with deadlines is a classic operations research problem that appears in OS scheduling, manufacturing planning, and project management. The greedy insight — schedule highest-paying jobs as late as possible within their deadlines — is counterintuitive: you might expect to schedule early, but scheduling late preserves earlier slots for potentially higher-value jobs that might arrive.

The Union-Find optimisation that reduces O(n^2) to O(n log n) is an elegant application of the disjoint set data structure in a non-graph context — a useful demonstration that Union-Find is about more than just connected components.

Greedy Algorithm

Sort jobs by profit descending. For each job, find the latest available slot before its deadline and assign it there.

This is the core greedy choice: take the highest-profit job and defer it as late as possible. Why late? Because early slots are scarce — a job with deadline 1 can only go in slot 1. If you take a low-profit job for slot 1, you block every job with deadline 1. By scheduling high-profit jobs late, you keep earlier slots flexible for jobs that have no choice but to go early.

job_scheduling.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def job_scheduling(jobs: list[tuple]) -> tuple[int, list]:
    """
    jobs: list of (job_id, deadline, profit)
    Returns (total_profit, scheduled_job_ids)
    """
    jobs = sorted(jobs, key=lambda x: x[2], reverse=True)
    max_deadline = max(d for _, d, _ in jobs)
    slots = [None] * (max_deadline + 1)  # slots[1..max_deadline]
    total = 0
    scheduled = []
    for job_id, deadline, profit in jobs:
        # Find latest free slot at or before deadline
        for slot in range(deadline, 0, -1):
            if slots[slot] is None:
                slots[slot] = job_id
                total += profit
                scheduled.append(job_id)
                break
    return total, scheduled

jobs = [('a',2,100), ('b',1,19), ('c',2,27), ('d',1,25), ('e',3,15)]
print(job_scheduling(jobs))  # (142, ['a', 'c', 'e'])
Output
(142, ['a', 'c', 'e'])
Production Insight
The naive scanning is O(n) per job, fine for <1000 jobs.
At 10k jobs, this becomes 100M iterations — noticeable latency.
If your scheduler runs every minute, that's unsustainable.
Rule: use naive only for small batches or prototypes.
Key Takeaway
Greedy choice = highest profit first.
Slot assignment = latest available slot ≤ deadline.
This combination yields maximum total profit.
Proven optimal by exchange argument.

Union-Find Optimisation — O(n log n)

The naive slot-finding is O(n) per job — O(n²) total. Using Union-Find (disjoint set), we can find the latest free slot in near-O(1) amortised time, giving O(n log n) overall.

How it works: treat each time slot as a node in a Union-Find structure. When a slot is used, union it with the slot before it (slot → slot-1). The find operation returns the largest free slot ≤ given deadline. It's essentially a 'next available lower slot' finder, with path compression making almost constant time.

job_scheduling_uf.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def find(parent, i):
    if parent[i] != i:
        parent[i] = find(parent, parent[i])
    return parent[i]

def job_scheduling_uf(jobs):
    jobs = sorted(jobs, key=lambda x: x[2], reverse=True)
    max_d = max(d for _, d, _ in jobs)
    parent = list(range(max_d + 2))  # extra sentinel for slot 0
    total, scheduled = 0, []
    for job_id, deadline, profit in jobs:
        slot = find(parent, deadline)
        if slot > 0:
            parent[slot] = slot - 1  # union with previous slot
            total += profit
            scheduled.append(job_id)
    return total, scheduled

# Same example yields same result
jobs = [('a',2,100), ('b',1,19), ('c',2,27), ('d',1,25), ('e',3,15)]
print(job_scheduling_uf(jobs))  # (142, ['a', 'c', 'e'])
Output
(142, ['a', 'c', 'e'])
Production Insight
Union-Find may over-allocate memory: parent array of size max_d+2.
If deadlines are sparse (max_d=1,000,000 but only 100 jobs), wasteful.
Alternative: use hash-based disjoint set to track only used slots.
Rule: choose Union-Find when deadlines are dense relative to job count.
Key Takeaway
Union-Find turns slot search from O(n) to O(α(n)).
α(n) ≤ 4 for any practical input — effectively constant.
Total time dominated by sort: O(n log n).
But only use it when n > ~5000; otherwise overhead negates benefit.

Complexity

Naive: O(n log n) sort + O(n²) slot finding = O(n²) Union-Find: O(n log n) sort + O(n α(n)) slot finding ≈ O(n log n)

For most interview purposes the naive O(n²) implementation is sufficient. But you should understand both — the interviewer may ask how to optimise when n reaches 10⁵.

Production Insight
O(n²) at n=10⁵ is 10¹⁰ operations — minutes of runtime.
O(n log n) at same size takes <0.1 seconds.
If your scheduler runs hourly, O(n²) might be acceptable for n<5000.
Always benchmark your actual n before optimising.
Rule: optimise only when you've measured the bottleneck.
Key Takeaway
Naive: O(n²) — fine for small n.
Union-Find: O(n log n) — essential for large n.
Sort is the unavoidable O(n log n) step.
Choose based on input size, not theory alone.

Proof of Optimality via Exchange Argument

We need to prove that the greedy algorithm (highest profit first, latest slot) produces maximum total profit. The exchange argument works like this:

  1. Let G be the set of jobs selected by greedy. Let O be an optimal set with maximum total profit.
  2. Compare G and O from highest profit to lowest. At the first point they differ, greedy chose job j (higher profit) while optimal chose job k (lower profit or none).
  3. Since j has higher profit and is scheduled at or after k's slot, we can swap k with j in O without reducing profit and without violating deadlines.
  4. After a finite number of swaps, O becomes identical to G, proving G is optimal.

This argument holds only because the profit-sorted order is monotonic — the greedy never picks a lower-profit job when a higher-profit one could be placed. The 'latest slot' rule ensures we don't block future jobs unnecessarily.

Why 'Latest Slot' Works
  • Slot 1 can only fit jobs with deadline ≥ 1 — every job qualifies.
  • Slot 10 can only fit jobs with deadline ≥ 10 — fewer jobs qualify.
  • So slot 1 is the scarcest: fill it last, with a job that absolutely must go there.
  • By scanning from deadline down to 1, you effectively 'push' each job as far right as possible.
Production Insight
Exchange argument is the standard correctness proof for greedy.
Interviewers expect you to outline it when asked 'why is this optimal?'
Real-world scheduler may violate the 'no two jobs same slot' assumption.
If jobs can run concurrently (e.g., multi-core), greedy fails entirely.
Rule: know the assumptions behind the proof — they tell you when not to use it.
Key Takeaway
Exchange argument shows greedy is optimal.
Proof structure: take first difference → swap → profit doesn't decrease.
Valid only when each job requires exactly one time unit.
If jobs have variable duration, the problem becomes NP-hard.

Variations and Real-World Extensions

The classic job sequencing problem assumes each job takes exactly one time unit. In practice, jobs have varying durations and may depend on each other. Here are common variations:

Weighted Interval Scheduling: Jobs have start/end times and durations. Greedy fails; DP required. Job Sequencing with Release Times: Each job has a release time (earliest start). Greedy still works with slight modification — always pick the job with highest profit among available ones. Multi-Machine Scheduling: Multiple workers/CPUs. NP-hard in general; heuristic approaches used. Minimum Lateness Scheduling: Instead of maximising profit, minimise maximum lateness. Solved by EDD (Earliest Due Date) — a different greedy.

Understanding the classic version provides the foundation for these extensions.

Production Insight
Classic job sequencing rarely appears in raw form in production.
But the 'greedy + union-find' trick shows up in resource allocation problems.
For example, assigning IP addresses from a pool: each allocation has an expiry (deadline) and a 'profit' (priority).
Rule: learn the pattern, not just the problem. It transfers to scheduling slots, memory blocks, time slots.
Key Takeaway
Classic problem is a special case — unit jobs, single machine.
Real schedulers are usually more complex.
But the greedy + union-find pattern generalises to many 'slot assignment' problems.
Master this, then explore variations.
Which Scheduling Algorithm to Use?
IfAll jobs have unit duration, single machine, maximise profit with deadlines
UseGreedy (this article) — optimal and fast.
IfJobs have start/end times, overlap not allowed, maximise profit
UseWeighted Interval Scheduling — DP (O(n log n)).
IfJobs have processing times and due dates, minimise max lateness
UseEarliest Due Date (EDD) — greedy.
IfMultiple identical machines, jobs have durations, minimise makespan
UseList Scheduling (greedy heuristic) — near optimal; exact is NP-hard.
● Production incidentPOST-MORTEMseverity: high

Missed Deadline in a Cloud Batch Scheduler Cost $50k in Penalties

Symptom
Critical reports consistently missed their deadlines by hours, while lower-priority batch jobs ran on time. The scheduler never flagged an error.
Assumption
The team assumed scheduling jobs as early as possible would leave room for urgent jobs later. They didn't account that later deadlines could be ignored if earlier slots were filled.
Root cause
The scheduler assigned each job to the first free slot at or after its deadline — not the latest free slot before its deadline. High-profit jobs with later deadlines were pushed to early slots, blocking themselves and leaving no space for earlier-deadline jobs.
Fix
Replaced the earliest-available-slot logic with the classic greedy approach: sort by profit descending and assign each job to the latest available slot ≤ its deadline. Added a validation test that checks total profit against a known optimal solution for small input sizes.
Key lesson
  • For deadline-based scheduling, always assign jobs to the latest possible slot — not the earliest.
  • Greedy is optimal here, but only when the choice rule and slot selection are both correct.
  • Unit test edge cases: all jobs same deadline, all profits equal, large profit disparity.
Production debug guideDiagnose why your greedy scheduler is producing suboptimal profit or skipping jobs that should fit.4 entries
Symptom · 01
Total profit is lower than expected from manual calculation.
Fix
Verify sorting: ensure profit is descending. Then check that you're scanning slots from deadline down to 1, not from 1 up to deadline.
Symptom · 02
All jobs fit but some high-profit ones are missing from output.
Fix
Check if you're using 1-indexed or 0-indexed time slots. A common bug: using slots[0] as day 1 but forgetting to offset max deadline.
Symptom · 03
Union-Find optimisation returns fewer jobs than naive version.
Fix
The parent array must size up to max deadline + 2 to avoid off-by-one. Also confirm find() compresses properly; missing path compression leads to O(n²) anyway.
Symptom · 04
Scheduler runs very slow for >10k jobs.
Fix
Upgrade from naive O(n²) to Union-Find O(n log n). Also confirm sort step is not O(n²) — use sorted() or sort() with key, not custom comparison.
★ Quick Job Sequencing TroubleshooterCheat-sheet commands and checks for debugging job scheduling algorithms.
Profit too low
Immediate action
Print the sorted job list and confirm profits are descending.
Commands
print(sorted(jobs, key=lambda x: x[2], reverse=True))
Check slot-assignment loop: for s in range(deadline, 0, -1):
Fix now
If you used range(1, deadline+1) instead, reverse it.
Union-Find returns 0 jobs+
Immediate action
Print parent array after init — it should be list(range(max_d + 2)).
Commands
print(parent[:10])
Check find() path compression: parent[i] = find(parent, parent[i])
Fix now
Set parent = list(range(max_d + 2)) not max_d + 1.
Greedy vs Union-Find Optimisation
CriterionNaive O(n²)Union-Find O(n log n)
Time complexity (after sort)O(n * max_deadline) → O(n²) worstO(n α(n)) ≈ O(n)
Space complexityO(max_deadline)O(max_deadline)
Code complexitySimple loop, no extra DSRequires Union-Find with path compression
Best for n< 5000≥ 10000
Off-by-one riskLow: slots[1..D] easyMedium: parent array size + sentinel
Memorisation difficultyStraightforwardModerate; need to recall parent[] = slot-1

Key takeaways

1
Sort by profit descending
greedily assign highest-profit jobs first.
2
Assign each job to the latest available slot ≤ its deadline.
3
Naive O(n²); Union-Find optimisation achieves O(n log n).
4
Exchange argument proves optimality
a core greedy proof technique.
5
Unit time per job is a critical assumption; drop it and the problem changes entirely.
6
This pattern transfers to any slot-based resource allocation under deadlines.

Common mistakes to avoid

5 patterns
×

Assigning jobs to earliest available slot instead of latest

Symptom
Suboptimal total profit — high-profit jobs with later deadlines get blocked by lower-profit jobs that fit earlier.
Fix
Change loop to scan from deadline down to 1: for slot in range(deadline, 0, -1):
×

Off-by-one in slot array indexing (0 vs 1 indexed)

Symptom
Jobs with deadline = 1 are never scheduled, or IndexOutOfBounds.
Fix
Allocate slots of size max_deadline + 1 and ignore index 0, or allocate max_deadline and shift all accesses by -1.
×

Using Union-Find but forgetting path compression in find()

Symptom
Slot search degrades to O(n) per job, giving O(n²) runtime despite using Union-Find.
Fix
Ensure find() sets parent[i] = find(parent, parent[i]) recursively, not just returns parent[i].
×

Sorting by deadline instead of profit

Symptom
Total profit is far from optimal — often low-profit jobs occupy slots while high-profit ones are skipped.
Fix
Sort by profit descending: jobs.sort(key=lambda x: x[2], reverse=True)
×

Assuming jobs can be split or run part-time

Symptom
Algorithm schedules partial jobs or miscalculates profit.
Fix
Explicitly clarify that each job occupies exactly one time slot, indivisible. For fractional jobs, use Fractional Knapsack instead.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Why do we assign jobs to the latest possible slot rather than earliest?
Q02SENIOR
What data structure optimises slot-finding to O(log n)?
Q03SENIOR
Prove that sorting by profit and taking the latest available slot is opt...
Q04SENIOR
How would you handle a variant where each job takes X time units instead...
Q05SENIOR
What if jobs have release times where they cannot be started before a ce...
Q01 of 05SENIOR

Why do we assign jobs to the latest possible slot rather than earliest?

ANSWER
Because earlier slots are more constrained: slot 1 can only fit jobs with deadline ≥ 1, but slot 10 can fit any job with deadline ≥ 10. By scheduling a high-profit job as late as possible, we keep the scarce early slots free for jobs that have no later option. This maximises the number of jobs we can fit and hence the total profit. The exchange argument proves optimality.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What if no slot is available for a job?
02
Why does the Union-Find parent array need to be size max_deadline + 2?
03
Can this algorithm handle duplicate profits?
04
What is the time complexity if we sort using insertion sort?
05
How does this relate to Earliest Deadline First (EDF) scheduling?
🔥

That's Greedy & Backtracking. Mark it forged?

3 min read · try the examples if you haven't

Previous
Fractional Knapsack Problem — Greedy Approach
10 / 13 · Greedy & Backtracking
Next
Huffman Encoding — Greedy Data Compression