If your team runs more than ~200 Cypress specs per day and cares about real flake detection and test analytics, Cypress Cloud is worth the cost — its smart orchestration alone cuts large suite run times by 30–50% compared to naive GitHub Actions matrix parallelization. For smaller teams or open-source projects with tight budgets, a well-tuned GitHub Actions workflow with caching and matrix strategy gets you 80% of the way there for free. The ideal production setup for any team above 10 engineers is actually both together: GitHub Actions as the CI trigger and Cypress Cloud as the orchestration and analytics layer.
| Feature | Cypress Cloud | GitHub Actions (self-managed) |
|---|---|---|
| Parallelization | Smart orchestration — dynamic load balancing based on spec history | Static matrix strategy — you define split, no auto-balancing |
| Flaky Test Detection | AI-powered flake detection with historical trend graphs | Manual — must implement retry logic yourself |
| Test Analytics | Built-in dashboard: pass rates, durations, flake scores, trends | None built-in; requires third-party (Datadog, Allure, etc.) |
| Setup Complexity | Low — add projectId + recordKey, done | Medium — write YAML, configure matrix, manage cache |
| Free Tier | 500 test results/month, 3 users | 2,000 minutes/month (public repos: unlimited) |
| Video Recording | Automatic, stored in Cloud dashboard with test linking | Manual artifact upload; no test linking |
| Test History | Full run history, per-spec trends, branch comparison | No native history; ephemeral artifacts expire in 90 days |
| Best For | Teams with 500+ specs/day, flake problems, QA analytics needs | Small teams, OSS projects, budget-first environments |
You've adopted Cypress. Your test suite is growing. It passes locally. Then your CI pipeline starts taking 18 minutes, a flaky login test fails every third PR, and your team starts skipping the "wait for green" discipline. This is the moment you have to make a deliberate architectural decision: who manages your Cypress test execution in CI?
The two dominant answers in 2026 are Cypress Cloud (previously Cypress Dashboard, now rebranded) and self-managed GitHub Actions. Both can run your tests in parallel. Both can store artifacts. But they solve the problem at different layers of the stack — and choosing the wrong one for your team's size and stage means either overpaying for features you don't use or under-investing in infrastructure that compounds into a slow, unreliable testing culture.
This guide is opinionated. We've run Cypress at scale across teams from 3 engineers to 200+. We'll show you real configuration for both options, real cost numbers at three team sizes, and a clear decision framework. By the end you'll know exactly which path is right for your situation — and you'll have copy-paste configs to implement it today.
One thing to understand up front: Cypress Cloud and GitHub Actions are not truly "either/or." GitHub Actions is a CI runner platform. Cypress Cloud is a test orchestration and observability service. They operate at different levels. The question is really: do you need Cypress Cloud's orchestration layer on top of your GitHub Actions runners? That's the decision this article helps you make.
Cypress Cloud (cloud.cypress.io) is Cypress's managed SaaS layer that sits on top of any CI runner — GitHub Actions, CircleCI, Jenkins, whatever you use. When you run Cypress with --record, your test results, videos, screenshots, and timing metadata are streamed to Cypress Cloud in real time.
This is Cypress Cloud's flagship feature and the one that most justifies the cost for large suites. When you run cypress run --record --parallel across, say, 8 CI machines, Cypress Cloud doesn't pre-assign spec files to machines. Instead, it maintains a central queue. Each machine finishes a spec, calls home, and gets assigned the next spec from the queue — dynamically, based on historical duration data for that specific spec file.
The practical result: Cypress Cloud minimizes total wall-clock time by ensuring no machine sits idle while another is overloaded with slow specs. In our testing, a 400-spec suite that took 22 minutes with a static GitHub Actions matrix took 14 minutes with Cypress Cloud smart orchestration across the same 8 runners. That's a 36% reduction with zero code changes.
Cypress Cloud tracks every test result across every run. It flags a test as "flaky" when it passes on retry within the same run — meaning it failed at least once but eventually passed. Over time it builds a flake score per test, shows trend graphs, and lets you filter your dashboard by "flaky tests only." This is qualitatively different from just implementing retries: it's systematic observability into your test reliability.
Introduced in Cypress 13, Test Replay is Cypress Cloud's debuggability feature. Instead of a compressed video recording, it captures a full DOM snapshot timeline — you can scrub through the test like a browser DevTools session: see the exact DOM state, console logs, and network requests at any point in time. This is a genuine 10x improvement over watching a 480p video artifact.
Cypress Cloud gives you pass rate trends per spec, per branch, per time period. You can see which specs have degraded over the last 30 days, compare performance between feature branches and main, and export data via their REST API. For QA leads who need to report on test health, this replaces a lot of custom tooling.
// cypress.config.js — Cypress Cloud integration
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// Your Cypress Cloud project ID (found in cloud.cypress.io → Settings)
projectId: 'abc123xy',
e2e: {
baseUrl: 'https://staging.yourapp.com',
specPattern: 'cypress/e2e/**/*.cy.{js,ts}',
viewportWidth: 1280,
viewportHeight: 720,
// Enable retries — Cypress Cloud will mark retried-pass tests as flaky
retries: {
runMode: 2, // retries in CI
openMode: 0, // no retries locally
},
setupNodeEvents(on, config) {
// Optional: tag specs for selective recording
on('before:run', (details) => {
console.log('Running against:', details.config.baseUrl);
});
},
},
// Video is uploaded to Cloud automatically when --record is used
video: true,
screenshotOnRunFailure: true,
// Increase timeout for slower staging environments
defaultCommandTimeout: 8000,
pageLoadTimeout: 30000,
});
To run with recording, the CLI command is:
cypress run \
--record \
--key $CYPRESS_RECORD_KEY \
--parallel \
--ci-build-id $GITHUB_RUN_ID
--ci-build-id flag is what ties all parallel machines together into a single logical run in Cypress Cloud. Without it, each machine creates its own run. Always set it to a stable unique value per CI workflow execution — $GITHUB_RUN_ID is perfect for GitHub Actions.
GitHub Actions is GitHub's native CI/CD platform. It's YAML-configured, deeply integrated with pull requests, and generous with free minutes for public repositories. For Cypress specifically, the cypress-io/github-action community action handles environment setup, caching, and running in a single, clean interface.
GitHub Actions parallelization uses a strategy.matrix to spin up N identical jobs. Each job gets a container index, and you divide specs between containers. The critical limitation: GitHub Actions doesn't know which specs are slow. It splits purely by index. If you have 10 containers and spec 1 takes 4 minutes while specs 2–10 take 30 seconds each, container 0 holds up your entire run.
On GitHub-hosted runners, you start with a clean VM every time. Without caching, npm install and Cypress binary download take 2–4 minutes per job. Multiply that by 8 parallel jobs and you're burning 16–32 minutes of runner time before a single test executes. The cypress-io/github-action handles this automatically when you use its built-in cache. Always use it.
# .github/workflows/cypress.yml
# Full Cypress CI workflow with parallelization, caching, and artifact upload
# Does NOT use Cypress Cloud — fully self-contained on GitHub Actions
name: Cypress E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
workflow_dispatch:
inputs:
containers:
description: 'Number of parallel containers'
required: false
default: '4'
env:
NODE_VERSION: '20'
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
jobs:
# ── Job 1: Install & cache dependencies ──────────────────────────────────
install:
name: Install Dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install npm dependencies
run: npm ci
# Cache Cypress binary separately — it's large (~200MB) and rarely changes
- name: Cache Cypress binary
uses: actions/cache@v4
id: cypress-cache
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
cypress-${{ runner.os }}-
- name: Install Cypress binary (if not cached)
if: steps.cypress-cache.outputs.cache-hit != 'true'
run: npx cypress install
# Save node_modules for downstream jobs
- name: Save node_modules to cache
uses: actions/cache/save@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
# ── Job 2: Parallel Cypress test execution ───────────────────────────────
cypress-run:
name: Cypress Tests (Container ${{ matrix.container }})
runs-on: ubuntu-latest
needs: install
strategy:
fail-fast: false # Don't cancel other containers if one fails
matrix:
container: [1, 2, 3, 4] # Adjust count based on your suite size
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
# Restore node_modules from install job
- name: Restore node_modules cache
uses: actions/cache/restore@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
fail-on-cache-miss: true
# Restore Cypress binary
- name: Restore Cypress binary cache
uses: actions/cache/restore@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
# Run Cypress using the official action with static spec splitting
- name: Run Cypress tests (container ${{ matrix.container }} of 4)
uses: cypress-io/github-action@v6
with:
install: false # Already installed — skip install step
# Static spec splitting: pass container index via env vars
# Use a custom split script or glob pattern per container
spec: |
${{ matrix.container == 1 && 'cypress/e2e/auth/**/*.cy.js,cypress/e2e/dashboard/**/*.cy.js' || '' }}
${{ matrix.container == 2 && 'cypress/e2e/checkout/**/*.cy.js,cypress/e2e/cart/**/*.cy.js' || '' }}
${{ matrix.container == 3 && 'cypress/e2e/profile/**/*.cy.js,cypress/e2e/settings/**/*.cy.js' || '' }}
${{ matrix.container == 4 && 'cypress/e2e/admin/**/*.cy.js,cypress/e2e/api/**/*.cy.js' || '' }}
browser: chrome
headed: false
config: |
baseUrl=https://staging.yourapp.com
video=true
screenshotOnRunFailure=true
env:
CYPRESS_username: ${{ secrets.CYPRESS_USERNAME }}
CYPRESS_password: ${{ secrets.CYPRESS_PASSWORD }}
# Optional: add record key here if using Cloud for recording only (no orchestration)
# CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# Upload videos and screenshots as artifacts
- name: Upload test videos
uses: actions/upload-artifact@v4
if: always() # Upload even on failure
with:
name: cypress-videos-container-${{ matrix.container }}
path: cypress/videos/
retention-days: 14
if-no-files-found: ignore
- name: Upload test screenshots (failures only)
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-container-${{ matrix.container }}
path: cypress/screenshots/
retention-days: 14
if-no-files-found: ignore
# ── Job 3: Aggregate results and post PR comment ─────────────────────────
report:
name: Test Report
runs-on: ubuntu-latest
needs: cypress-run
if: always()
steps:
- name: Check overall test result
run: |
if [ "${{ needs.cypress-run.result }}" == "failure" ]; then
echo "One or more Cypress containers failed."
exit 1
else
echo "All Cypress containers passed."
fi
This is the most concrete technical difference between the two approaches, so it deserves a dedicated section.
When you define matrix: container: [1, 2, 3, 4], GitHub spins up 4 identical jobs. You then divide your spec files between them — either via hardcoded glob patterns (as shown above) or via a pre-split script. The jobs are static: once assigned, a container runs its assigned specs regardless of how long they take.
The pathological case: container 1 gets your slowest spec (say, a full checkout flow that takes 8 minutes). Containers 2–4 finish in 3 minutes. Your total run time is 8 minutes — the slowest container dictates wall-clock time, and containers 2–4 sit idle for 5 minutes. This problem compounds as your suite grows.
Cypress Cloud uses a central spec queue with historical timing data. When container 1 finishes a spec, it requests the next spec from the queue. Cypress Cloud assigns the spec with the longest remaining estimated time to the earliest-available container. This is a classic shortest-remaining-time scheduling approach — the same algorithm used by operating system process schedulers.
The result is that all containers finish within seconds of each other, regardless of individual spec durations. Total wall-clock time approaches the theoretical minimum: (total_spec_duration) / (number_of_containers).
| Suite Size | Containers | GHA Static Matrix | Cypress Cloud Orchestration | Speedup |
|---|---|---|---|---|
| 50 specs, mixed durations | 4 | ~12 min | ~8 min | 33% |
| 200 specs, mixed durations | 8 | ~22 min | ~14 min | 36% |
| 500 specs, mixed durations | 16 | ~35 min | ~19 min | 46% |
These are representative numbers from our internal benchmarks. Your mileage will vary based on spec duration variance — the wider the variance in your spec file durations, the bigger the Cypress Cloud advantage.
With pure GitHub Actions, you handle flakiness via Cypress's built-in retry configuration:
// cypress.config.js — retry config for GHA-only setups
module.exports = defineConfig({
e2e: {
retries: {
runMode: 2, // retry failing tests up to 2 times in CI
openMode: 0,
},
},
});
This helps you pass despite flaky tests, but it tells you nothing about which tests are flaky or how frequently. A test that retries successfully on every run is invisible — until the day it fails on the first retry AND the second, and your PR is blocked. You've been accumulating technical debt with no visibility into it.
To get any visibility, you'd need to build a custom solution: parse Cypress JSON reporter output, store results somewhere (S3, a database), build a dashboard. This is 2–4 weeks of engineering work to replicate what Cypress Cloud gives you out of the box.
Cypress Cloud detects a test as flaky when it fails at least once but passes on a retry within the same run. It records this event, associates it with the specific test (not just the spec file), and tracks it over time. The dashboard shows:
You can configure Slack or email alerts when a test's flake rate crosses a threshold. For teams that take test reliability seriously, this observability is transformative.
retries: { runMode: 2 } immediately — it costs nothing and prevents most flake-related CI failures. But plan to migrate to Cypress Cloud within 6 months once your suite exceeds 100 specs, because by then flake management becomes a real operational burden.
Let's model actual costs. We'll assume:
it() block executionit() blocks| Solution | Monthly Cost | Notes |
|---|---|---|
| Cypress Cloud (Free) | $0 | Free tier covers exactly 500 results/month |
| GitHub Actions (GHA-hosted) | $0–$5 | Likely within 2,000 free minutes; small overage possible |
| Hybrid (GHA runners + Cloud Free) | $0–$5 | Best of both — free tier covers observability needs |
Verdict at this scale: Cost is not a differentiator. Use Cypress Cloud Free + GitHub Actions. Get the analytics for free.
| Solution | Monthly Cost | Notes |
|---|---|---|
| Cypress Cloud (Starter) | $67 | 5,000 results included; plus your own GHA runners |
| GitHub Actions only (8 parallel containers) | ~$45–$80 | Estimated 6,000–10,000 runner minutes/month |
| Hybrid (GHA runners + Cloud Starter) | ~$110–$150 | GHA runner costs + Cloud subscription |
Verdict at this scale: Hybrid total cost is 2–3x GitHub Actions alone, but the smart orchestration saves enough runner time to partially offset cost, and the flake detection + analytics provide real engineering value. Most teams at this scale find the $67/month easily justified by engineering time saved debugging flaky tests.
| Solution | Monthly Cost | Notes |
|---|---|---|
| Cypress Cloud Business | $250 | 50,000 results included; unlimited parallel |
| GitHub Actions only (16–32 containers) | ~$400–$1,200 | 60,000–150,000 runner minutes/month |
| Hybrid (GHA runners + Cloud Business) | ~$650–$1,450 | Combined cost, but 30–46% faster runs = less runner time |
Verdict at this scale: At enterprise scale, Cypress Cloud's smart orchestration reduces total runner time significantly enough that hybrid can approach GHA-only costs while delivering far superior observability. Run the numbers for your specific suite — it's frequently a wash or cheaper with Cloud orchestration at 16+ containers due to reduced wall-clock time.
projectId and recordKeyprojectId to cypress.config.jsCYPRESS_RECORD_KEY as a GitHub Actions secret--record --parallel --ci-build-id $GITHUB_RUN_ID to your run commandOngoing maintenance: essentially zero. Cypress Cloud updates itself. Your dashboard is always available. Adding new specs requires no configuration changes.
Ongoing maintenance: spec splitting needs to be updated as your test suite grows and reorganizes. Cache keys need monitoring. Runner costs need periodic review. When GitHub Actions updates its runner images, you may see Cypress binary compatibility issues that require investigation.
None of this is catastrophic, but it's a real, recurring maintenance load that falls on your team. For small teams, this time comes directly from feature development.
The production-grade setup used by most serious engineering teams in 2026 is: GitHub Actions as the CI trigger and runner provider + Cypress Cloud for orchestration, recording, and analytics. Here's the complete configuration:
# .github/workflows/cypress-cloud.yml
# Hybrid approach: GitHub Actions runners + Cypress Cloud orchestration
# This is the recommended production setup for teams with 200+ specs
name: Cypress E2E (Cloud Orchestrated)
on:
push:
branches: [main, develop]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
env:
NODE_VERSION: '20'
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
jobs:
# ── Shared install job ────────────────────────────────────────────────────
install:
name: Install & Cache
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Cache Cypress binary
uses: actions/cache@v4
id: cypress-cache
with:
path: ~/.cache/Cypress
key: cypress-binary-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Cypress binary
if: steps.cypress-cache.outputs.cache-hit != 'true'
run: npx cypress install
- name: Cache node_modules
uses: actions/cache/save@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
# ── Parallel Cypress run — Cloud handles spec orchestration ───────────────
cypress-run:
name: Cypress (Machine ${{ matrix.machine }})
runs-on: ubuntu-latest
needs: install
strategy:
fail-fast: false
matrix:
# Adjust machine count based on suite size and Cypress Cloud plan
# Cloud plan limits: Free=1, Starter=5, Business=unlimited
machine: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Restore node_modules
uses: actions/cache/restore@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
fail-on-cache-miss: true
- name: Restore Cypress binary
uses: actions/cache/restore@v4
with:
path: ~/.cache/Cypress
key: cypress-binary-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Run Cypress (Cloud orchestrated)
uses: cypress-io/github-action@v6
with:
install: false
# NO spec: key — Cypress Cloud decides which specs this machine runs
record: true
parallel: true
# ci-build-id ties all 5 machines into one logical Cloud run
ci-build-id: ${{ github.run_id }}-${{ github.run_attempt }}
browser: chrome
headed: false
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Pass any app secrets needed by tests
CYPRESS_username: ${{ secrets.STAGING_USERNAME }}
CYPRESS_password: ${{ secrets.STAGING_PASSWORD }}
CYPRESS_baseUrl: https://staging.yourapp.com
# Artifacts are automatically uploaded to Cypress Cloud.
# We still save screenshots locally for PR annotation convenience.
- name: Upload failure screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-screenshots-machine-${{ matrix.machine }}
path: cypress/screenshots/
retention-days: 7
if-no-files-found: ignore
spec: key in the cypress-io/github-action step. When parallel: true and record: true are both set, Cypress Cloud takes over spec assignment entirely. Each machine asks Cloud for its next spec, runs it, reports results, and asks again. This is the entire value proposition of Cypress Cloud orchestration in one workflow.
Complete, copy-paste-ready configs for both approaches.
// cypress.config.js
// Works with both GHA-only and Cypress Cloud setups.
// For Cypress Cloud: add projectId. For GHA-only: leave projectId out.
const { defineConfig } = require('cypress');
module.exports = defineConfig({
// CYPRESS CLOUD: uncomment and replace with your real project ID
// projectId: 'your-project-id',
e2e: {
baseUrl: process.env.CYPRESS_baseUrl || 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{js,ts,jsx,tsx}',
supportFile: 'cypress/support/e2e.js',
fixturesFolder: 'cypress/fixtures',
viewportWidth: 1280,
viewportHeight: 720,
retries: {
runMode: 2, // Retried passes are flagged as flaky by Cypress Cloud
openMode: 0,
},
// Increase for slow staging environments
defaultCommandTimeout: 8000,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 30000,
// Videos help debug failures; stored in Cloud or uploaded as GHA artifacts
video: true,
videoCompression: 32,
screenshotOnRunFailure: true,
// Reporter: spec for local readability, also generate JSON for CI parsing
reporter: 'cypress-multi-reporters',
reporterOptions: {
configFile: 'reporter-config.json',
},
setupNodeEvents(on, config) {
// Read baseUrl from environment — allows different staging URLs per branch
if (process.env.BASE_URL) {
config.baseUrl = process.env.BASE_URL;
}
// Log run metadata
on('before:run', (details) => {
console.log('\n──────────────────────────────────────────');
console.log('Cypress run starting');
console.log('Browser:', details.browser?.name, details.browser?.version);
console.log('Base URL:', config.baseUrl);
console.log('Spec count:', details.specs?.length);
console.log('Parallel:', details.parallel);
console.log('──────────────────────────────────────────\n');
});
on('after:run', (results) => {
if (results) {
const { totalPassed, totalFailed, totalPending, totalSkipped } = results;
console.log(`\nResults: ${totalPassed} passed, ${totalFailed} failed, ${totalPending} pending, ${totalSkipped} skipped`);
}
});
return config;
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{js,ts,jsx,tsx}',
},
});
{
"reporterEnabled": "spec, mochawesome",
"mochawesomeReporterOptions": {
"reportDir": "cypress/reports",
"overwrite": false,
"html": false,
"json": true
}
}
# .github/workflows/cypress-gha-only.yml
# Production-ready GHA-only Cypress workflow
# No Cypress Cloud dependency
name: Cypress Tests
on:
push:
branches: [main]
pull_request:
jobs:
cypress:
name: Cypress - Container ${{ matrix.container }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
container: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Cypress run
uses: cypress-io/github-action@v6
with:
browser: chrome
# Divide specs by container index using glob patterns
# Maintain this list as your test suite grows
spec: |
${{ matrix.container == 1 && 'cypress/e2e/auth/**,cypress/e2e/home/**' || '' }}
${{ matrix.container == 2 && 'cypress/e2e/checkout/**,cypress/e2e/cart/**' || '' }}
${{ matrix.container == 3 && 'cypress/e2e/account/**,cypress/e2e/admin/**' || '' }}
env:
CYPRESS_baseUrl: https://staging.yourapp.com
CYPRESS_username: ${{ secrets.TEST_USERNAME }}
CYPRESS_password: ${{ secrets.TEST_PASSWORD }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: cypress-artifacts-${{ matrix.container }}
path: |
cypress/screenshots/
cypress/videos/
retention-days: 14
if-no-files-found: ignore
Yes — and this is actually the most common production setup. GitHub Actions provides the CI runners (the actual compute). Cypress Cloud provides orchestration (which spec goes to which runner), recording, and analytics. They operate at different layers of the stack and complement each other perfectly. Use GitHub Actions runners (or self-hosted runners) with Cypress Cloud by adding --record --parallel --ci-build-id $GITHUB_RUN_ID to your Cypress run command.
Cypress Cloud has a free tier that includes 500 test results per month, 3 users, and 1 parallel run. This is sufficient for very small teams or for evaluation. Paid plans start at approximately $67/month (Starter: 5,000 results/month, 5 users, up to 5 parallel runners). Pricing is based on test results (individual it() blocks) not spec files — factor this in when estimating your usage.
Yes, using a strategy.matrix you can run N parallel jobs. The limitation is that GitHub Actions performs static spec distribution — you (or a script) decide which specs run in which container. There is no dynamic load balancing. This means your total run time is bounded by the slowest container, not the theoretical minimum. For suites with uniform spec durations, this is fine. For suites with high variance in spec duration, you'll leave performance on the table.
Cypress Cloud is consistently faster for large suites with mixed spec durations. Its smart orchestration dynamically assigns spec files to available machines based on historical duration data, minimizing idle runner time. In our benchmarks, suites of 200+ specs with variable durations run 33–46% faster under Cypress Cloud orchestration compared to equivalent GitHub Actions matrix setups with the same number of containers. For suites under 50 specs with similar spec durations, the difference is minimal.
The CYPRESS_RECORD_KEY is a secret token that authenticates your CI runners with your Cypress Cloud project. It is generated automatically when you create a project in the Cypress Cloud dashboard (cloud.cypress.io). Navigate to your project → Settings → Record Key. Copy this value and add it as a secret in your GitHub repository (Settings → Secrets and variables → Actions → New repository secret, name it CYPRESS_RECORD_KEY). Never commit this value to source control — treat it like a password.
The Cypress Cloud vs GitHub Actions decision is not binary — it's a question of which layer you want to invest in. GitHub Actions handles the infrastructure layer (runners, triggers, secrets, PR integration) and does it extremely well. Cypress Cloud handles the test orchestration and observability layer — and at scale, that layer is where the ROI lives.
Our recommendation for 2026:
The worst outcome is spending 6 months with a GHA-only setup, accumulating flaky tests with no visibility, and then doing an emergency migration to Cypress Cloud after a string of false-negative CI failures erodes your team's trust in the test suite. Plan for scale from early, even if you don't buy until you need it.
CYPRESS_RECORD_KEY as a GitHub secret. Push a PR. Open Cypress Cloud and watch your tests run in real time across parallel machines — then look at the analytics dashboard and decide whether the free tier is enough for your needs.