Choosing the right CircleCI resource class is the single biggest factor in balancing build speed and cost.
Let’s see how a simple Python build changes across resource classes. Imagine a circleci/python:3.9 image.
# On small
circleci build --job python-test --checkout=<commit-hash> --resource-class small
# On medium
circleci build --job python-test --checkout=<commit-hash> --resource-class medium
# On large
circleci build --job python-test --checkout=<commit-hash> --resource-class large
# On xlarge
circleci build --job python-test --checkout=<commit-hash> --resource-class xlarge
You’ll observe that small might take 5 minutes and cost $0.008/minute, while xlarge might take 1 minute and cost $0.04/minute. The total cost for a single run is time * price. So, 5 * 0.008 = $0.04 for small, and 1 * 0.04 = $0.04 for xlarge. For a single run, the cost is the same! But if you run this build 100 times a day, the small class costs $12/month, while the xlarge costs $120/month. The real win is finding the sweet spot where the time reduction outweighs the increased per-minute cost.
The core problem resource classes solve is providing predictable, isolated compute environments for your CI/CD jobs. Without them, you’d be running on shared infrastructure with wildly varying performance, making your builds slow and unreliable. CircleCI abstracts this into distinct tiers: small, medium, large, and xlarge. Each tier offers more CPU and RAM, directly impacting how quickly your build steps can execute.
Internally, these resource classes map to different instance types on cloud providers. For example, a small might be a general-purpose m5.large on AWS, while an xlarge could be a compute-optimized c5.4xlarge. The key difference is the CPU-to-RAM ratio and raw processing power. More powerful instances can run your build steps – compiling, testing, packaging – in parallel or with higher throughput.
You control this directly in your .circleci/config.yml file within the resource_class key under your job’s docker or macos executor.
jobs:
build-and-test:
docker:
- image: cimg/go:1.18
resource_class: medium # <--- This is what you change
steps:
- checkout
- run:
name: Build
command: go build ./...
- run:
name: Test
command: go test ./...
To pick the right class, you need to benchmark. Run your job on each available resource class and record the execution time and the corresponding cost. Plot this data: time on the Y-axis, resource class on the X-axis, and cost per run as a separate metric. You’re looking for the point where a further increase in resource class yields diminishing returns in time savings, or where the cost per run starts to significantly increase without a proportional speedup.
Most people think of resource classes as just "more power," but the underlying architecture is also crucial. For I/O-bound tasks (like cloning large repositories or downloading many small dependencies), a higher-tier instance with faster network throughput and potentially faster local storage can make a bigger difference than just raw CPU. Conversely, for CPU-bound tasks (like heavy compilation or complex simulations), you’re primarily looking for more cores and faster clock speeds.
The one thing most people don’t know is that the number of concurrent jobs you run on a given plan also interacts with resource class selection. If you have many jobs that could run in parallel, but they all contend for the same limited pool of small runners, they’ll queue up, and the effective build time increases dramatically. Sometimes, upgrading to a medium or large for a few critical, time-consuming jobs can free up small runners for other tasks, leading to a net reduction in overall pipeline execution time and potentially even cost if your plan has per-minute charges that are triggered by total pipeline duration.
The next step after optimizing resource classes is understanding how to parallelize your tests within a single job.