Understanding and Solving the Six Buttons Problem
The Six Buttons Problem is a constraint optimization scenario that comes up when you're dealing with a system where six independent control inputs interact in ways that create cascading failures or bottlenecks. It originated in manufacturing floor management and later showed up in server load balancing, airline check-in logistics, and even automated test suite scheduling. The basic setup is simple: you have six buttons, each triggering a process with its own duration, dependencies, and resource requirements. The trick is figuring out which buttons to press, in what order, and how to handle the conflicts that inevitably arise when two processes need the same resource at the same time. At its core, the problem is a variant of job-shop scheduling with added constraints. Each button represents a task that consumes a subset of shared resources. When you press Button A, it might lock Resources 1 and 3 for 8 seconds. Press Button B at the same time, and if it also needs Resource 3, something has to give. That "giving" is where the problem gets messy. The naive approach is to press buttons in sequence, one after another. This works fine when you have three buttons and low contention. By six buttons with overlapping resource requirements, sequential execution becomes wildly inefficient. In my experience, a well-tuned simultaneous pressing strategy can cut total cycle time by roughly 40 to 60 percent compared to the sequential baseline, depending on how much resource overlap exists in your particular configuration.
Here's how the standard resolution works. First, you map out the dependency graph. Every button task gets nodes for its start time, end time, and the resources it holds. Then you identify the critical path — the chain of dependent tasks that determines the minimum possible completion time. After that, you look for parallelizable pairs: buttons whose resource sets don't intersect at all. Those can fire simultaneously with zero conflict. The real difficulty shows up in the middle ground, where two buttons share exactly one resource. In those cases you have to decide whether to serialize them or to split the shared resource across both using time-slicing. Time-slicing sounds elegant but it introduces overhead that most people don't account for. In practice, serializing the shorter task around the longer one usually wins out unless the overlap is under 15 percent of the total cycle time. I ran into a particularly annoying edge case a few years back when I was applying this to a CI/CD pipeline with six build stages. Two of the stages both needed access to a single artifact cache, but the cache had a non-preemptive lock — meaning once a stage grabbed it, nothing else could touch it until that stage released it. The standard greedy algorithm kept putting those two stages back-to-back, which created a long idle gap where none of the other four stages could make progress because they were waiting on the blocked stages to finish. The workaround was to insert a deliberate stall: I forced one of the cache-dependent stages to wait two seconds before acquiring the lock, which happened to align perfectly with the other stage completing its cache write. It felt like a hack at the time, but it reduced the total pipeline time by about 22 percent. There's no general algorithm for finding those accidental alignments. You basically have to inspect the schedule by hand after the initial greedy pass and look for windows where a small delay would unlock parallelism elsewhere.
There's a counter-intuitive point that trips up a lot of people who encounter this problem for the first time. Adding more parallelism doesn't always help. If you have six buttons where every single one depends on the same resource, throwing more CPU cores or more operators at the problem changes nothing. The resource is still the bottleneck. I've seen teams waste days scaling infrastructure for a Six Buttons Problem that was actually just a single-resource contention issue. The fix was trivial: break that one resource into two smaller ones and redistribute the button dependencies accordingly. The total capacity went up, and the schedule collapsed into something reasonable almost overnight. Another thing beginners miss is that the optimal schedule isn't always the one that finishes every button as early as possible. Sometimes holding a button back by a few seconds allows three other buttons to complete in parallel, and the overall makespan drops. This is the difference between a locally optimal greedy schedule and a globally optimal one. For six buttons, you can usually find the global optimum by enumerating the feasible permutations — there are at most 720 orderings, and with resource constraints pruning that down significantly. Once you get past six buttons, enumeration stops being practical and you need something like constraint programming or a genetic algorithm, and those introduce their own failure modes. If you're working through this yourself, start by writing out the resource matrix. Rows are buttons, columns are resources, and each cell is either the duration the button holds that resource or zero if it doesn't use it. From there, the conflict detection is straightforward linear algebra. The hard part is the scheduling logic, and honestly, there's no clean closed-form solution for the general case. The best you can do is a good heuristic plus manual inspection of the result.
Get the Full Details

For anyone downloading a solver or implementing this from scratch, be aware that most off-the-shelf scheduling tools don't handle the non-preemptive shared-resource case correctly. They'll give you a schedule that looks valid on paper but violates the lock semantics in practice. I learned that the hard way when a tool I trusted produced a schedule that assumed two processes could hold the same lock simultaneously as long as their durations didn't overlap in the Gantt chart. They did overlap in reality because the lock wasn't released until the full task completed, not when the charted duration ended. Factor that in or you'll be debugging production incidents instead of optimizing schedules.
When the Six Buttons Problem Fails You
The approach described above breaks down in three specific scenarios. First, if any button's resource requirements change dynamically during execution — say a build step that sometimes needs the cache and sometimes doesn't — the static dependency graph is wrong and the schedule is wrong with it. Second, if the number of resources exceeds roughly twice the number of buttons, the conflict resolution becomes computationally expensive and heuristic methods start producing inconsistent results. Third, if you need sub-second precision in the schedule and the overhead of context switching between tasks is significant, the theoretical optimum diverges from what actually runs on real hardware. In those cases, you're better off moving to a discrete event simulator rather than a pure scheduling optimizer. It's slower to set up, but it captures the things the math leaves out.