Summary
Timely, an EdTech company that helps US schools build academic schedules, ran its React frontend on an NX monorepo with a GitHub Actions CI pipeline. This was another engagement with Timely, following our earlier work on its school scheduling architecture. This time, the bottleneck was not inside the scheduling product itself, but inside the engineering workflow that helped the team ship it.
The pipeline worked correctly, but every pull request had become slow and expensive. Developers waited around 30 minutes for checks to finish, long enough that most would switch to something else and lose the thread of what they were doing.
The workflow ran separate jobs for linting, type checks, tests, coverage checks, and build validation. Over time, it had accumulated repeated work across these jobs, including multiple dependency installs, duplicate test execution, and unnecessary main branch coverage generation.
We optimized the existing setup by restructuring the workflow, improving caching, and reusing artifacts, without changing the CI provider, the runners, or the core tooling.
The payoff showed up where the team felt it most. PR wait time dropped from about 30 minutes to about 10, so developers could stay on a task instead of context-switching while CI ran. Issues surfaced earlier in the run, clean PRs moved to merge sooner, and the projected GitHub Actions bill came down by 73%.
About the Client
Timely builds scheduling software for US schools. Its platform turns a tangle of courses, teachers, rooms, and student requirements into a workable timetable, one of the messier operational problems a school faces each year. Because districts plan around fixed academic calendars and enrollment windows, Timely's engineers ship on tight, deadline-driven cycles. In that kind of environment a slow CI pipeline is not just an annoyance: every stretch of time lost waiting on a build is time not spent getting features in front of schools before their planning window closes.
Industry: EdTech
Country: United States
The Challenge
Every pull request puts developers in a holding pattern. They waited around 30 minutes for CI to finish before they knew a change was safe to merge, long enough to break focus and switch to other work.
Underneath that wait, the pipeline was burning 60+ billed compute minutes per PR run. GitHub Actions bills each parallel job independently, so even though jobs ran at the same time, compute kept adding up across every runner.
The slowest job was the coverage check, which took around 32 minutes because it generated both PR branch coverage and main branch baseline coverage during every pull request run.
The original workflow looked like this:
| Job | Original behaviour | Approx. time |
| Lint check | Install dependencies + run lint | ~3 min |
| Type check | Install dependencies + run typecheck | ~3 min |
| Tests | Install dependencies + run tests | ~17 min |
| Coverage check | Install dependencies + run PR coverage + main coverage | ~32 min |
| Build validation | Install dependencies + run NX build | ~5 min |
The goal was to reduce avoidable work without changing the CI provider, moving to larger runners, or introducing new infrastructure.
Our Approach
We focused on the workflow's three biggest sources of repeated work, plus a smaller test-runner tuning.
1. Repeated dependency installation
Each job installed dependencies independently, even when the dependency set had not changed.
We introduced a reusable setup workflow that generated a cache key from package-lock.json, cached the full node_modules directory, and restored it in downstream jobs. This reduced dependency setup from around 2 minutes per job to about 10 seconds on cache hits.
2. Main branch coverage generation
The coverage job needed a main branch baseline to compare against the PR branch coverage. Earlier, the workflow generated this baseline by rerunning the main branch test suite on every PR.
We changed this by uploading the main branch coverage output as a GitHub Actions artifact after successful main branch runs. PR workflows could then download the latest successful artifact instead of regenerating it every time, with a fallback for cases where no main branch coverage artifact was available yet.
3. Duplicate PR test execution
The PR test suite was running twice: once in the test job and again in the coverage-check job.
We updated the test job to generate the PR coverage output during the normal test run. The coverage-check job then downloaded the PR coverage artifact and compared it with the main branch baseline, which turned the coverage-check job from a full test execution into a lightweight comparison step. That single change took the coverage check from around 32 minutes to roughly 6 seconds.
4. Jest worker configuration
We also tuned the test job for the standard GitHub-hosted runner by explicitly configuring Jest with --maxWorkers=2, so the test run made better use of the available runner resources.
The Results
For the team, the change is most visible in two places: the time developers lose waiting, and the monthly bill.
PR wait time dropped from about 30 minutes to about 10, so developers get feedback while a change is still fresh. Issues surface early in the run, and PRs that look good move to merge sooner rather than sit in a queue. Billed compute per PR fell far enough to take the GitHub Actions bill from about $180 to $48 a month.
| Metric | Before | After |
| Billed compute per PR run | 60+ minutes | ~16 minutes |
| Wall-clock CI time | ~30 minutes | ~10 minutes |
| Coverage check job | ~32 minutes | ~6 seconds |
| Dependency install per job | ~2 minutes | ~10 seconds on cache hit |
| Estimated saving per PR run | n/a | ~45 billed minutes |
At approximately 500 CI runs per month, the projected GitHub Actions cost dropped by 73% monthly.
After optimization, the individual jobs completed in approximately:
| Job | Optimized time |
| Lint check | ~1 min 10 sec |
| Type check | ~1 min 10 sec |
| Tests | ~10 min 10 sec |
| Coverage check | ~6 sec |
| Build validation | ~3 min 10 sec |
Technology Stack
- GitHub Actions
- React
- NX Monorepo
- Babel
- Webpack
- npm
- Jest
- actions/cache@v4
- actions/upload-artifact@v4
- actions/download-artifact@v4
- actions/github-script@v7
Why Procedure
We find bottlenecks inside existing workflows
Procedure does not start by replacing tools or adding infrastructure. We first study how the current engineering system behaves, where time is being repeated, and which parts of the workflow can be simplified without affecting reliability.
We improve CI/CD without overcomplicating the setup
In this project, the gains came from better caching, artifact reuse, and cleaner job responsibilities. The CI provider, runners, and core tooling stayed the same, which made the optimization practical and easier to maintain.
We optimize for developer feedback loops
A faster pipeline is not only a cost improvement. It helps developers get PR feedback sooner, reduce context switching, and move work forward with fewer delays.


