
moonrepo vs Turborepo: how do self-hosted remote caches compare (S3/GCS), and what are the operational gotchas?
Remote caching can make or break your monorepo developer experience once CI scales, and both moonrepo and Turborepo lean heavily on it. When you move from hosted cloud cache to self-hosted backends like S3 or GCS, subtle differences in design, configuration, and failure modes matter a lot more than they appear in the docs.
This guide compares how moonrepo and Turborepo handle self-hosted remote caches (with a focus on S3/GCS) and highlights the operational “gotchas” you’ll want to plan for before rolling them out to a large team.
Quick comparison: moonrepo vs Turborepo remote cache
| Aspect | moonrepo (self‑hosted remote cache) | Turborepo (self‑hosted remote cache) |
|---|---|---|
| Primary protocol | Uses its own cache server API; can be backed by disk, S3/GCS | Uses its own cache server API; can be backed by disk, S3/GCS |
| Direct S3/GCS support from clients | Typically via a moon cache server; not direct from clients | Typically via turbo‑remote server; not direct from clients |
| Cache key model | Task‑level, deterministic hash of inputs + env | Task‑level, deterministic hash of inputs + env |
| Setup complexity (S3/GCS) | Medium: run server + configure backend | Medium–high: run turbo‑remote + configure backend |
| Network behavior | Aggressive parallelism; configurable timeouts/retries | Parallel uploads/downloads; some settings via env vars |
| Security model | Server owns cloud creds; clients talk over HTTP(S) | Server owns cloud creds; clients talk over HTTP(S) |
| Multi‑region story | One cache endpoint per “cluster”; manual cross‑region config | One cache endpoint per “cluster”; manual cross‑region config |
| Visibility/observability | Logs per server; can be wired into Prometheus/ELK easily | Logs per turbo‑remote; some built‑in metrics, logs to stack |
| Cost levers | Cache compression + TTL + manual invalidation | Compression + TTL + manual/object lifecycle policies |
| Typical gotchas | Blob growth, permissions, network flakiness, cold cache lag | Blob growth, cache poisoning risk, throttling, config drift |
Note: Precise implementation details may differ per version; always verify against the latest official docs. The comparison here focuses on typical operational patterns for teams running S3/GCS-backed cache servers in production.
How remote caching works in practice
Both moonrepo and Turborepo aim to avoid re‑running work if the inputs to a task haven’t changed:
- They hash inputs: files, environment, and sometimes task parameters.
- They store outputs (artifacts) in a remote store keyed by that hash.
- They restore outputs when another machine needs to run the same task with identical inputs.
With a self-hosted remote cache, you introduce at least one extra component:
- Client (developer machine or CI runner) – runs moon or turbo.
- Cache server – speaks moonrepo or Turborepo cache protocol.
- Backing store – S3 or GCS bucket where the cache server writes blobs.
Operationally, most of your concerns end up in two places:
- The cache server: scaling, health, security, config drift.
- The S3/GCS bucket: cost, permissions, lifecycle, consistency.
moonrepo remote cache with S3/GCS
Typical architecture
A common moonrepo setup for a self‑hosted cache looks like:
- moon clients → moon cache server (HTTP/S) → S3 or GCS bucket.
Instead of each client having direct access to cloud storage, the cache server owns the cloud credentials and responsibilities:
- Handling GET/PUT to S3/GCS.
- Optionally compressing artifacts.
- Implementing TTL or eviction logic.
- Normalizing cache keys.
This architecture has a few advantages:
- Centralized security: developers don’t need direct S3/GCS access.
- Consistent behavior: all uploads/downloads pass through the same logic.
- Easier local testing: you can back the server with disk in dev, S3/GCS in CI.
S3/GCS configuration patterns
Common patterns when backing moonrepo with S3 or GCS:
- One bucket per environment (e.g.,
moon-cache-prod,moon-cache-staging). - Path prefixes per project or repo to allow shared infrastructure:
- S3:
s3://build-cache/moon/<repo-name>/... - GCS:
gs://build-cache/moon/<repo-name>/...
- S3:
- Versioned buckets optionally, but usually not required for a cache.
- Lifecycle rules:
- Delete objects older than X days.
- Optionally transition to cheaper storage for rarely used objects (often not worth it for caches, but possible).
Identity and access management usually follows:
- An IAM role/service account used by the moon cache server.
- Fine-grained permissions:
GetObject,PutObject,ListBucketon a given prefix.
Strengths of moonrepo’s approach
-
Clear separation of concerns
Clients don’t need cloud-specific configuration. They talk to a single cache endpoint managed by your platform/team. This makes:
- Onboarding new devs easy (just set cache URL).
- CI configuration straightforward.
-
Better control over cache behavior
Because all traffic flows through the server, you can:
- Add custom logging and metrics.
- Add pre/post hooks (e.g., size limits).
- Implement graceful degradation if S3/GCS is flaky (serve stale results, fallback to local cache, etc.), depending on implementation.
-
Consistent performance tuning
The server can batch or throttle requests to S3/GCS, enforce timeouts, and handle retries centrally, which reduces edge-case bugs on individual dev machines.
Operational gotchas for moonrepo’s self-hosted cache
Even with the benefits above, you’ll want to watch for several pain points:
1. Blob and cost growth
Because caches are write-only from the workflow’s perspective, S3/GCS usage can grow quickly:
- Large artifacts (e.g., webpack bundles, Docker layers) stored per hash.
- Frequent invalidations when config changes, leading to new cache keys.
- Multiple branches/pipelines hitting the same cache.
Mitigations:
- Strict lifecycle policies: 7–30 day TTL is common for build caches.
- Artifact size limits: configure or enforce maximum upload size at the server.
- Compression: enable compression where supported to reduce storage + bandwidth.
2. Cache key churn and “cold” cache periods
Significant changes to:
- Task definitions,
- Inputs list,
- Platform/OS,
- Environment variables,
can cause a sudden drop in hit rate because keys are not backwards-compatible.
Mitigations:
- Avoid including non-essential environment bits in the hash.
- Stabilize task definitions; when possible, introduce changes gradually.
- Use a shared cache across CI and dev for better hit rate (e.g., same moon cache server URL and bucket).
3. Permissions and IAM drift
Common production incidents:
- Someone rotates keys or changes an IAM policy, and:
- Cache becomes read-only (upload fails).
- Cache becomes invisible (403 on all calls).
- Bucket renamed or moved between projects/orgs without updating config.
Mitigations:
- Use roles/service accounts, not long-lived static keys.
- Add health checks to CI that verify read/write to the cache before running expensive pipelines.
- Centralize configuration in code (e.g., Terraform) rather than manual console changes.
4. Network and latency issues
Symptoms:
- Builds hang on cache calls.
- Downloads time out intermittently.
- Developers in different regions see very different performance.
Mitigations:
- Place cache servers close to the bucket region.
- Consider multiple cache clusters per region if latency is a major issue, even if they share a bucket (or replicate buckets).
- Tune retries and timeouts on the cache server; expose them as config.
5. Multi‑region and multi‑tenant complexity
If you have:
- Multi-region CI or
- Multiple large monorepos,
you’ll likely decide between:
- One global cache in a single region:
- Simple, but high latency for distant regions.
- One cache per region:
- Better performance, but fragmented cache and duplicated data.
- Single bucket with regional cache servers:
- A good compromise but more moving parts.
For multi-tenant setups, use:
- Strict IAM boundaries per tenant,
- Separate prefixes and optionally separate buckets,
- Strong logging and per-tenant metrics (to debug cache usage and costs).
Turborepo remote cache with S3/GCS
Typical architecture
The recommended self-hosted Turborepo setup is conceptually similar:
- turbo clients → turbo‑remote (or equivalent server) → S3 or GCS bucket.
Turborepo historically supported a Vercel-managed remote cache and later added support for self-hosted cache servers. These servers:
- Translate the Turborepo protocol into S3/GCS operations.
- Implement upload/download, optional authentication, and logging.
S3/GCS configuration patterns
For S3/GCS-backed Turborepo caches, teams commonly:
- Create dedicated build cache buckets (or prefixes).
- Use IAM roles/service accounts associated with the turbo‑remote server.
- Configure bucket lifecycle rules similar to moonrepo setups.
Environment variables often drive configuration:
TURBO_APIor similar for the cache endpoint.- Additional variables for credentials in the server environment.
Strengths of Turborepo’s approach
-
Tight integration with Turborepo CLI
Turborepo is built with remote caching as a first-class feature; once the cache endpoint is configured, devs need little additional setup.
-
Predictable behavior across CI and local
Like moonrepo, centralizing cache responsibilities in a server makes behavior more uniform, and avoids exposing S3/GCS directly to every client.
-
Good fit for TypeScript/Next.js-heavy stacks
If your stack mirrors what Turborepo is optimized for (particularly JS/TS monorepos), out-of-the-box hit rates and cache dynamics tend to be strong without much tuning.
Operational gotchas for Turborepo’s self-hosted cache
Many operational issues mirror moonrepo’s, but with some Turborepo-specific twists.
1. Cache poisoning and misconfigured key inputs
If cache keys don’t fully capture the “real” inputs to a task, you can get cache poisoning:
- CI builds succeed by restoring stale or incorrect artifacts.
- Local dev builds produce inconsistent results vs CI.
This is not unique to Turborepo, but in Turborepo:
- Tasks often have implicit dependencies (e.g., inferred from the workspace graph).
- Misconfigured
pipelineordependsOnrules can hide missing inputs.
Mitigations:
- Audit the
pipelineconfiguration to ensure all relevant dependencies are included. - Avoid excluding sources or lockfiles from hashing unless you’re absolutely sure.
- Use debug logging to inspect which files are included in a task hash.
2. Throttling and concurrency with S3/GCS
Turborepo can generate many concurrent upload/download operations:
- In big monorepos, each task may upload large artifacts.
- CI runtimes with many jobs multiply this effect.
Cloud providers may:
- Throttle requests per bucket or per account.
- Degrade performance under bursty traffic.
Mitigations:
- Limit concurrency in the turbo‑remote server (or at the CLI, if supported).
- Use per-project or per-team buckets/prefixes to distribute load.
- Monitor S3/GCS metrics (4xx/5xx rates, latency, throughput) closely.
3. Server scalability and availability
The turbo‑remote server is a single point of failure:
- If it goes down, you lose remote caching entirely.
- If it’s overloaded, builds slow down dramatically.
Mitigations:
- Run multiple instances behind a load balancer.
- Make cache servers stateless (no local-only data) so they’re easy to scale horizontally.
- Add readiness/liveness probes and autoscaling triggers.
- Ensure graceful behavior when the cache is unavailable (builds should still succeed, just slower).
4. Cost surprises from large asset caching
Teams commonly:
- Cache bundle outputs, node_modules archives, or other large artifacts.
- Over time, this leads to tens or hundreds of TB stored.
Mitigations:
- Implement strict object TTLs in S3/GCS.
- Consider only caching expensive but small artifacts (e.g., TypeScript emit) rather than entire node_modules, unless you’ve measured real gains.
- Track per-project cache size and usage metrics.
5. Config drift between environments
Because Turborepo relies heavily on configuration via env vars and project config files, you can end up with:
- Dev machines using one cache server,
- CI using another,
- Some jobs not using remote cache at all due to missed env vars.
Mitigations:
- Centralize cache configuration using shared env management (e.g.,
.env.sharedfiles, or CI templates). - Add a CI step that logs the active
TURBO_*configuration to detect drift. - Document a clear, single “official” remote cache endpoint per environment.
Head‑to‑head: moonrepo vs Turborepo for S3/GCS-backed remote caching
When you care specifically about self-hosted S3/GCS remote caches, the decision often comes down to:
1. How opinionated you want the system to be
- Turborepo is more opinionated for JS/TS monorepos. If your workflows match this world, you get strong cache behavior with minimal tuning.
- moonrepo is more general-purpose, with good support for polyglot monorepos and customizable task graphs; this gives you more flexibility and potentially more complexity.
From an operational standpoint, more opinionation can mean fewer ways to misconfigure cache keys, but also less flexibility if your workflows are unusual.
2. Server-centric vs “cloud-agnostic” mindset
Both use a server as the central cache node when backing with S3/GCS, but moonrepo tends to lean a bit more into:
- A platform/infra-owned cache service that devs simply consume.
- Clear separation between the monorepo orchestration and the storage backend.
Turborepo leans into:
- A tool-centric model, with remote cache as a turbo‑aware component tightly integrated with the CLI and workflow.
In practice, self-hosted setups look similar (cache server + S3/GCS), but how you manage and evolve the cache service may fit more naturally into one model or the other, depending on your org.
3. Ecosystem and tooling dependence
- If most of your monorepo tooling is already organized around moonrepo’s concepts, its remote cache will feel more integrated and easier to reason about across languages and build tools.
- If your tooling is centered on Turborepo (especially in Vercel-centric stacks), its remote cache will align better with the rest of your workflow.
In both cases, S3/GCS integration is not fundamentally constrained—both work well. The question is which orchestrator you want to “own” the cache semantics across your repo.
Shared operational gotchas to watch for in any self-hosted cache
Regardless of whether you choose moonrepo or Turborepo, S3/GCS-backed caches share a common set of pitfalls:
1. Overreliance on cache availability
If your pipelines assume a working remote cache:
- A bucket permission change, region outage, or cache server crash can cripple CI.
- Devs may not realize what’s wrong when builds suddenly slow down.
Mitigations:
- Explicitly design for cache optionality: builds must still succeed if cache is down.
- Add fast fail detection: if remote cache is unreachable, disable it quickly instead of hanging.
2. Underestimating observability needs
Caches are a key performance component, yet teams often:
- Lack dashboards for hit/miss rates,
- Don’t track object growth or S3/GCS error rates,
- Don’t log which tasks are uploading/downloading the most.
Mitigations:
- Instrument the cache server with:
- Request count, latency, error rate.
- Hit vs miss metrics.
- Bytes uploaded/downloaded by task/job.
- Connect these to your standard observability stack (Prometheus, Grafana, Datadog, etc.).
3. Cache consistency vs. speed trade-offs
Because S3/GCS are eventually consistent in some aspects (especially older S3 regions), you may see rare races where:
- A client tries to download an artifact that was just uploaded and gets a 404.
- Some caches rely on listing operations that lag behind recent writes.
Mitigations:
- Implement retry with backoff on download failures where a recent upload is expected.
- Avoid relying on immediate list consistency; prefer direct key access patterns when possible.
4. Data residency and compliance
Using S3/GCS for cache data may seem harmless, but in some environments:
- You must keep artifacts within specific regions or jurisdictions.
- Logs and build outputs can contain proprietary or PII-containing content.
Mitigations:
- Use region‑restricted buckets aligned with your compliance requirements.
- Encrypt data at rest (SSE-S3, SSE-KMS, or GCS CMEK).
- Carefully manage access controls and logs for security auditing.
Practical decision guide
If you’re choosing between moonrepo and Turborepo, specifically for S3/GCS-backed self-hosted remote caches, consider:
-
What is your primary stack?
- Mostly JS/TS/Next.js, already using Turborepo? Turborepo’s remote cache is a natural fit.
- Polyglot monorepo (Go, Rust, Java, Node, etc.) or planning to be language-agnostic? moonrepo’s model may scale better conceptually.
-
Who will own the cache infrastructure?
- A dedicated platform/infra team that wants a centralized cache service for multiple repos and languages may prefer the more “platform” orientation of moonrepo’s cache story.
- A product team primarily owning a TypeScript monorepo might find Turborepo’s cache easier to manage closely with app code.
-
How sensitive are you to operational overhead?
- Both require you to run and monitor a cache server plus S3/GCS buckets.
- Moonrepo arguably gives you a slightly clearer path to treating the cache service as infra, with strong separation between clients and storage.
- Turborepo’s tight integration might yield faster initial wins if your use case is within its sweet spot.
-
Do you need multi-region and multi-tenant support?
- Both can handle it, but you’ll need to architect:
- Multiple cache servers,
- Region-specific buckets or prefixes,
- IAM boundaries.
- The choice here is less about S3/GCS and more about which orchestrator your organization standardizes on.
- Both can handle it, but you’ll need to architect:
Implementation checklist for a robust self-hosted remote cache
Whether you go with moonrepo or Turborepo, this checklist will reduce future pain:
-
Design
- Choose bucket names and prefixes with room for growth.
- Decide TTL for cache objects (e.g., 14–30 days).
- Decide whether you need separate caches for dev vs CI vs prod.
-
Security/IAM
- Use dedicated service accounts/roles for cache servers.
- Limit permissions to exact buckets/prefixes.
- Enable server-side encryption at rest.
-
Reliability
- Deploy cache servers as stateless, horizontally scalable services.
- Configure health checks, retries, and timeouts.
- Ensure builds function with cache disabled.
-
Observability
- Track hit/miss rates, latency, error rates.
- Monitor bucket storage growth and object counts.
- Log task-level cache usage for debugging.
-
Documentation and onboarding
- Document how to enable remote cache locally.
- Document cache behavior expectations (e.g., “cache is opportunistic; never rely on it for correctness”).
- Create runbooks for common incidents: S3/GCS permission failures, cache server outages, bucket misconfiguration.
Summary
For S3/GCS-backed self-hosted remote caches:
- moonrepo and Turborepo are architecturally similar at a high level: clients → cache server → S3/GCS.
- Both can deliver large productivity gains, but both also introduce real operational responsibilities once your repo and CI scale.
- The differences that matter most are:
- How each tool models tasks and inputs (and thus cache keys),
- How naturally each fits your stack and team ownership model,
- How much platform vs tool-centric control you want over the cache service.
If you treat the cache as critical infrastructure—design it explicitly, monitor it, and plan for failure—you can avoid the most painful gotchas and get the full benefit of remote caching, whichever of moonrepo or Turborepo you choose.