Skip to main content
Case study

From 30 Minutes to 10: Speeding Up CI for an EdTech Team's React Monorepo

Restructuring a GitHub Actions pipeline for an EdTech scheduling platform: caching, artifact reuse, and cleaner job responsibilities took PR wait time from ~30 to ~10 minutes and cut projected CI cost by 73%.

Client
Confidential ClientEdTech
Industry
EdTechProduct Engineering
Services
DevOps Engineering · Platform EngineeringEngineering & delivery
Engagement
Embedded teamBuild & scale
The outcome
3x
Faster PR Feedback
73%
Lower CI Cost
6s
Coverage Check

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:

JobOriginal behaviourApprox. time
Lint checkInstall dependencies + run lint~3 min
Type checkInstall dependencies + run typecheck~3 min
TestsInstall dependencies + run tests~17 min
Coverage checkInstall dependencies + run PR coverage + main coverage~32 min
Build validationInstall 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.

MetricBeforeAfter
Billed compute per PR run60+ 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 runn/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:

JobOptimized 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.

Tags:DevOps EngineeringPlatform Engineering
Let’s build

Have a platform that’s outgrowing its architecture?

We embed senior engineers to untangle the hard parts — multi-tenancy, migrations, scale — without stopping your roadmap. First call is with an engineer, not a salesperson.

No obligation30-minute callEngineers, not sales