Software Teams

CI/CD Pipeline Optimization: From 40 Minutes to 5

Actionable techniques for optimizing CI/CD pipelines including parallelization, caching, test splitting, and build layer optimization.

Speed Is a Feature of Your Pipeline

A slow CI/CD pipeline is not just an inconvenience. It changes how developers work. When a pipeline takes 40 minutes, developers batch up changes, context-switch to other tasks, and lose focus. When it takes 5 minutes, they push small changes frequently, get fast feedback, and stay in flow.

Pipeline speed directly affects deployment frequency, change failure rate, and developer satisfaction. Optimizing it is one of the highest-leverage investments an engineering team can make.

Measure Before You Optimize

Before changing anything, instrument your pipeline. You need to know:

  • Total wall-clock time from push to deployment
  • Time per stage (checkout, install, build, test, deploy)
  • Queue wait time (how long jobs wait for a runner)
  • Flaky test rate (how often tests fail for reasons unrelated to code changes)
  • Cache hit rates (are your caches actually working?)

Most CI platforms provide some of this data. For detailed analysis, export pipeline logs and build a dashboard. You cannot optimize what you cannot see.

Parallelization

The most impactful optimization is running independent stages in parallel instead of sequentially.

Before: Lint -> Unit Tests -> Integration Tests -> Build -> Deploy (sequential, 40 min)

After:

Stage 1 (parallel):  Lint | Unit Tests | Integration Tests    [15 min]
Stage 2:             Build                                      [3 min]
Stage 3:             Deploy                                     [2 min]
                                                        Total: 20 min

Within test stages, split tests across multiple parallel runners:

test:
  parallel: 4
  script:
    - php artisan test --parallel --processes=4

Most CI platforms support parallelism natively. GitHub Actions uses matrix strategies, GitLab CI uses the parallel keyword, and CircleCI has parallelism built into its configuration.

Test Splitting Strategies

When splitting tests across parallel runners, even distribution matters. Three approaches:

  • File-based splitting. Divide test files evenly across runners. Simple but uneven since test files vary in execution time.
  • Timing-based splitting. Use historical timing data to distribute tests so each runner finishes at roughly the same time. Libraries like knapsack handle this automatically.
  • Failure-based ordering. Run recently failed tests first. This gives developers faster feedback on likely failures.

Dependency Caching

Installing dependencies from scratch on every build wastes minutes. Cache them.

Composer (PHP):

cache:
  key: composer-${{ hashFiles('composer.lock') }}
  paths:
    - vendor/

npm/Node:

cache:
  key: node-${{ hashFiles('package-lock.json') }}
  paths:
    - node_modules/

Key insight: Cache by lockfile hash. When dependencies change, the cache invalidates automatically. When they do not change, installation takes seconds instead of minutes.

Also cache your build artifacts between stages. Do not rebuild assets in the deploy stage that were already built in the build stage.

Docker Layer Caching

If your pipeline builds Docker images, layer caching is essential. Docker caches each layer and reuses unchanged layers on subsequent builds.

Optimize your Dockerfile for caching:

# Dependencies change less often - cache this layer
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts

# Application code changes frequently - this layer rebuilds often
COPY . .
RUN composer dump-autoload --optimize

Order instructions from least-frequently-changed to most-frequently-changed. A dependency change should not invalidate the layer that copies your source code.

Database Optimization for Tests

Database setup is often the hidden bottleneck in test pipelines. Strategies to speed it up:

Use an in-memory database for unit tests. SQLite in-memory mode eliminates disk I/O entirely. Be aware of dialect differences if your production database is MySQL or PostgreSQL.

Use database transactions for test isolation. Wrap each test in a transaction and roll back after. This is much faster than migrating and seeding before each test.

Template databases. Create a fully migrated and seeded database once, then clone it for each parallel test runner. PostgreSQL's CREATE DATABASE ... TEMPLATE makes this trivial.

Pre-built database images. For integration tests that need a specific database state, build a Docker image with the database pre-loaded. Pull it in your pipeline instead of running migrations every time.

Reducing Build Times

Incremental compilation. For frontend assets, tools like Vite and esbuild are dramatically faster than webpack. If you are still on webpack, switching to Vite can cut frontend build times from minutes to seconds.

Tree shaking. Ensure your build tooling eliminates unused code. A production JavaScript bundle should not include development dependencies.

Asset fingerprinting with cache invalidation. Use content hashes in filenames (app.a1b2c3.js). This allows aggressive browser caching while ensuring updates are picked up immediately.

Eliminating Flaky Tests

Flaky tests (tests that sometimes pass and sometimes fail without code changes) are pipeline poison. They erode trust in the pipeline and cause developers to retry builds repeatedly.

Track flakiness systematically. Flag tests that fail on retry. Most CI platforms can be configured to retry failed tests automatically. Log these retries and review the list weekly.

Common causes and fixes:

  • Time-dependent tests. Use Carbon::setTestNow() or equivalent to freeze time.
  • Order-dependent tests. Run tests in random order to surface hidden dependencies.
  • External service dependencies. Mock external APIs. Never let your test suite depend on a third-party service being available.
  • Race conditions. In parallel test suites, ensure tests do not share mutable state.

Pipeline as Code

Store your pipeline configuration in version control alongside your application code. Review pipeline changes in pull requests just like application code. This ensures pipeline modifications are deliberate, documented, and reversible.

The Feedback Loop

A fast pipeline creates a virtuous cycle. Developers push smaller changes more often. Smaller changes are easier to review, easier to test, and easier to roll back if something goes wrong. Invest in your pipeline and it pays dividends in everything your team ships.

Let's talk about your software teams needs

Whether you're modernizing your infrastructure, navigating compliance, or building new software - we can help.

Book a 30-min Call