Blazingly fast Turborepo remote cache server written in Rust as a Github Action with Docker support for Linux and MacOS
See the code![]()
Turborepo remote cache server, API-compliant as a GitHub Action or Docker with S3-compatible storage support.
Make sure that you have an S3-compatible storage available. We currently tested with:
The GitHub Action supports both Linux (x64 and arm64) and macOS (x64 and arm64 via a universal binary) runners. Here is how to use it:
In your workflow files, add the following global environment variables:
env:
TURBO_API: "http://127.0.0.1:8585"
TURBO_TEAM: "NAME_OF_YOUR_REPO_HERE"
# TURBO_TOKEN is required by Turborepo to enable remote caching.
# The cache server only validates it when TURBO_TOKEN is also set
# on the server itself — otherwise the server accepts any value.
# See the "Authentication" section below for details.
TURBO_TOKEN: "secret-turbo-token"
In the same workflow file, after checking out your code, start the Turbo Cache Server in the background:
- name: Checkout repository
uses: actions/checkout@v4
- name: Turborepo Cache Server
# ALWAYS use a pinned version of the action
# As we don't ship the latest versions of the binary on the main branch
# PLEASE see the latest versions here:
# https://github.com/brunojppb/turbo-cache-server/releases
uses: brunojppb/turbo-cache-server@4.0.3
env:
PORT: "8585"
S3_BUCKET_NAME: your-bucket-name-here
# Region defaults to "eu-central-1"
S3_REGION: "eu-central-1"
# Optional: If you need to provide specific auth keys, separate from default AWS credentials
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
# Optional: If not using AWS, provide endpoint like `https://rustfs` for your instance.
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
# Optional: If your S3-compatible store does not support requests
# like https://bucket.hostname.domain/. Setting `S3_USE_PATH_STYLE`
# to true configures the S3 client to make requests like
# https://hostname.domain/bucket instead.
# Defaults to "false"
S3_USE_PATH_STYLE: false
# Optional: Enable server-side encryption for stored artifacts.
# Valid values: AES256, aws:kms, aws:kms:dsse, aws:fsx
S3_SERVER_SIDE_ENCRYPTION: "AES256"
# Now you can run your turborepo tasks and rely on the cache server
# available in the background to provide previously built artifacts (cache hits)
# and let Turborepo upload new artifacts when there is a cache miss.
- name: Run tasks
run: turbo run test build typecheck
```
And that is all you need to use our remote cache server for Turborepo. As a reference, take a look at this example workflow file for inspiration.
[!NOTE] These environment variables are required by Turborepo so it can call the Turbo Cache Server with the right HTTP body, headers and query strings. These environment variables are necessary so the Turborepo binary can identify the Remote Cache feature is enabled and can use them across all steps. You can read more about this here on the Turborepo official docs.
For folks using Gitlab or any other CI environment that supports Docker, you can run the Turbo Cache Server as a docker container:
docker run \
-e S3_ACCESS_KEY=KEY \
-e S3_SECRET_KEY=SECRET \
-e S3_BUCKET_NAME=my_cache_bucket \
-e S3_ENDPOINT=https://s3_endpoint_here \
-e S3_REGION=eu \
-e S3_SERVER_SIDE_ENCRYPTION=AES256 \
# Optional: enables authentication. See "Authentication" below.
-e TURBO_TOKEN=secret-turbo-token \
-p "8000:8000" \
ghcr.io/brunojppb/turbo-cache-server:latest
Turbo Cache Server runs without authentication by default. This is an intentional design decision: in the vast majority of deployments the server sits behind a private network (a VPC, a Kubernetes cluster, or a GitHub Actions runner) where only trusted sources can reach it, and requiring a shared token adds overhead without a meaningful security benefit.
To enable authentication, set the TURBO_TOKEN environment variable on the
server. When set, every incoming request must include an
Authorization: Bearer <TURBO_TOKEN> header or it will be rejected with
401 Unauthorized. Turborepo clients read their own TURBO_TOKEN env var
and send this header automatically, so the server-side and client-side
values must match.
The server reads the Bearer scheme in any case, as HTTP requires. It
compares the token itself in constant time, so the time a rejection takes
does not tell an attacker how much of a guess was correct. A rejected
request returns a JSON body that says whether the header was missing or the
token was wrong:
{ "error": "Invalid TURBO_TOKEN" }
When TURBO_TOKEN is unset on the server, the authentication middleware is
bypassed entirely and any Authorization header on incoming requests is
ignored.
[!TIP] If you expose the cache server to the public internet, or to networks you do not fully control, you should set
TURBO_TOKENon the server.
Turbo Cache Server is a cloud-native, stateless application that runs seamlessly in Kubernetes environments. This makes it ideal for horizontally scaled deployments across multiple pods.
kubectl configured to access your clusterBelow is a minimal setup to deploy Turbo Cache Server to your Kubernetes cluster:
apiVersion: v1
kind: Secret
metadata:
name: turbo-cache-s3-credentials
namespace: default
type: Opaque
stringData:
S3_ACCESS_KEY: "your-access-key-here"
S3_SECRET_KEY: "your-secret-key-here"
# Optional: omit to run without authentication. See "Authentication" above.
TURBO_TOKEN: "secret-turbo-token"
Apply the secret:
kubectl apply -f turbo-cache-secret.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: turbo-cache-server
namespace: default
spec:
replicas: 2 # Scale horizontally as needed
selector:
matchLabels:
app: turbo-cache-server
template:
metadata:
labels:
app: turbo-cache-server
spec:
containers:
- name: turbo-cache-server
image: ghcr.io/brunojppb/turbo-cache-server:latest
ports:
- containerPort: 8000
name: http
env:
- name: PORT
value: "8000"
- name: S3_BUCKET_NAME
value: "your-bucket-name-here"
- name: S3_REGION
value: "eu-central-1"
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: S3_ACCESS_KEY
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: S3_SECRET_KEY
# Optional: only needed when authentication is enabled.
- name: TURBO_TOKEN
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: TURBO_TOKEN
- name: S3_ENDPOINT
value: "https://your-s3-endpoint.com"
- name: S3_SERVER_SIDE_ENCRYPTION
value: "AES256"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /management/health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /management/health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
Apply the deployment:
kubectl apply -f turbo-cache-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: turbo-cache-server
namespace: default
spec:
selector:
app: turbo-cache-server
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
Apply the service:
kubectl apply -f turbo-cache-service.yaml
Artifacts are uploaded to S3 using streaming rather than full in-memory buffering. There is no server-side setting for enforcing payload size, so size limits should be enforced by your reverse proxy, ingress, load balancer, or storage policy if you need them.
If you need to expose the service externally:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: turbo-cache-ingress
namespace: default
annotations:
# Configure based on your ingress controller
# nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: turbo.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: turbo-cache-server
port:
number: 8000
Apply the ingress:
kubectl apply -f turbo-cache-ingress.yaml
Once deployed, configure your Turborepo clients to use the Kubernetes service:
export TURBO_API="http://turbo-cache-server.default.svc.cluster.local:8000"
export TURBO_TEAM="your-team-name"
export TURBO_TOKEN="secret-turbo-token"
For external access through ingress:
export TURBO_API="https://turbo.yourdomain.com"
export TURBO_TEAM="your-team-name"
export TURBO_TOKEN="secret-turbo-token"
As your cache grows over time, you may want to automatically expire old cache entries to control storage usage and costs. Since Turbo Cache Server uses S3-compatible storage, you can configure bucket lifecycle rules to automatically delete objects after a specified period.
[!NOTE] Lifecycle rules are configured at the S3 bucket level, not within the Turbo Cache Server itself. This allows you to manage storage independently of the cache server configuration.
Object expiration is based on the last modified time of objects in your bucket. You can configure expiration in two ways:
For AWS S3, Cloudflare R2, RustFS, and other S3-compatible providers, you can use the AWS CLI to configure lifecycle rules.
Create a JSON file named lifecycle.json with the following content:
{
"Rules": [
{
"Status": "Enabled",
"Expiration": {
"Days": 30
}
}
]
}
Then apply the lifecycle configuration to your bucket:
aws s3api put-bucket-lifecycle-configuration \
--bucket your-bucket-name \
--lifecycle-configuration file://lifecycle.json
You can also set a specific expiration date:
{
"Rules": [
{
"Status": "Enabled",
"Expiration": {
"Date": "2025-12-31T00:00:00Z"
}
}
]
}
Turbo Cache Server includes built-in support for OpenTelemetry, providing distributed tracing and metrics collection out of the box. This enables you to monitor your cache server's performance and troubleshoot issues using industry-standard observability tools.
By default, all traces and metrics are tagged with the service name decay (the internal Rust crate name). You'll see this identifier in your observability platform when filtering or querying telemetry data. To use a different identifier, set the OTEL_SERVICE_NAME environment variable:
export OTEL_SERVICE_NAME="turbo-cache-server"
The OpenTelemetry integration works with all major observability SaaS platforms and open-source tools:
If you don't need telemetry, you can disable the OpenTelemetry SDK entirely by setting the OTEL_SDK_DISABLED environment variable to true. This follows the OpenTelemetry specification for disabling the SDK.
export OTEL_SDK_DISABLED="true"
When disabled, no OTLP exporters or system metric collectors will be initialized, and no connections will be attempted to any collector endpoint. Standard console and file logging will continue to work normally.
To enable OpenTelemetry export, set the following environment variables:
# OTLP endpoint (gRPC by default)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
# Or use HTTP protocol
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
# Optional: override the service name reported to your
# observability platform (defaults to "decay")
export OTEL_SERVICE_NAME="turbo-cache-server"
For platform-specific configurations:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.datadoghq.com"
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=<your-api-key>"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io"
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=<your-api-key>"
For local testing, you can use the provided Docker Compose setup that includes RustFS, Jaeger, and Prometheus:
docker-compose -f docker-compose.otel.yml up
This starts:
Then run your cache server with:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
cargo run
Visit Jaeger to see distributed traces and Prometheus to query metrics from your local cache server.
Turbo Cache Server is a tiny web server written in Rust that uses any S3-compatible bucket as its storage layer for the artifacts generated by Turborepo.
Here is a diagram showing how the Turbo Cache Server works within our actions during a cache hit:
sequenceDiagram
actor A as Developer
participant B as GitHub
participant C as GitHub Actions
participant D as Turbo Cache Server
participant E as S3 bucket
A->>+B: Push new commit to GH.<br>Trigger PR Checks.
B->>+C: Trigger CI pipeline
C->>+D: turborepo cache server via<br/>"use: turbo-cache-server@4.0.3" action
Note right of C: Starts a server instance<br/> in the background.
D-->>-C: Turbo cache server ready
C->>+D: Turborepo executes task<br/>(e.g. test, build)
Note right of C: Cache check on the Turbo cache server<br/>for task hash "1wa2dr3"
D->>+E: Get object with name "1wa2dr3"
E-->>-D: object "1wa2dr3" exists
D-->>-C: Cache hit for task "1wa2dr3"
Note right of C: Replay logs and artifacts<br/>for task
C->>+D: Post-action: Shutdown Turbo Cache Server
D-->>-C: Turbo Cache server terminates safely
C-->>-B: CI pipline complete
B-->>-A: PR Checks done
When a cache isn't yet available, the Turbo Cache Server will handle new uploads and store the artifacts in S3 as you can see in the following diagram:
sequenceDiagram
actor A as Developer
participant B as GitHub
participant C as GitHub Actions
participant D as Turbo Cache Server
participant E as S3 bucket
A->>+B: Push new commit to GH.<br>Trigger PR Checks.
B->>+C: Trigger CI pipeline
C->>+D: turborepo cache server via<br/>"use: turbo-cache-server@4.0.3" action
Note right of C: Starts a server instance<br/> in the background.
D-->>-C: Turborepo cache server ready
C->>+D: Turborepo executes build task
Note right of C: Cache check on the server<br/>for task hash "1wa2dr3"
D->>+E: Get object with name "1wa2dr3"
E-->>-D: object "1wa2dr3" DOES NOT exist
D-->>-C: Cache miss for task "1wa2dr3"
Note right of C: Turborepo executes task normaly
C-->>C: Turborepo executes build task
C->>+D: Turborepo uploads cache artifact<br/>with hash "1wa2dr3"
D->>+E: Put object with name "1wa2dr3"
E->>-D: Object stored
D-->>-C: Cache upload complete
C->>+D: Post-action: Turbo Cache Server shutdown
D-->>-C: Turbo Cache server terminates safely
C-->>-B: CI pipline complete
B-->>-A: PR Checks done
Turbo Cache Server requires Rust 1.75 or above. To setup your environment, use the rustup script as recommended by the Rust docs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Now run the following command to run the web server locally:
cargo run
During local development, you might want to try the Turbo Dev Server locally against a JS monorepo. As it depends on a S3-compatible service for storing Turborepo artifacts, we recommend using RustFS with Docker with the following command:
docker run -d \
--name rustfs_container \
-p 9000:9000 \
-p 9001:9001 \
-v $(pwd)/s3_data:/data \
-v $(pwd)/s3_logs:/logs \
-e RUSTFS_ACCESS_KEY=rustfsadmin \
-e RUSTFS_SECRET_KEY=rustfsadmin \
-e RUSTFS_CONSOLE_ENABLE=true \
rustfs/rustfs:latest \
--address :9000 \
--console-enable \
--access-key rustfsadmin \
--secret-key rustfsadmin \
/data
Copy the .env.example file, rename it to .env and add the environment
variables required. As we use RustFS locally, open the
Web UI, create a bucket, and use rustfsadmin for
both S3_ACCESS_KEY and S3_SECRET_KEY in the .env file.
To execute the test suite, run:
cargo test
While running our end-to-end tests, you might run into the following error:
thread 'actix-server worker 9' panicked at /src/index.crates.io-6f17d22bba15001f/actix-server-2.4.0/src/worker.rs:404:34:
called `Result::unwrap()` on an `Err` value: Os { code: 24, kind: Uncategorized, message: "Too many open files" }
thread 'artifacts::list_team_artifacts_test' panicked at tests/e2e/artifacts.rs:81:29:
Failed to request /v8/artifacts
This is likely due the the maximum number of open file descriptors defined for your user. Just run the following command to fix it:
ulimit -n 1024
Rust
88.1%
JavaScript
9.5%
Dockerfile
2.3%
Blazingly fast Turborepo remote cache server written in Rust as a Github Action with Docker support for Linux and MacOS
See the code![]()
Turborepo remote cache server, API-compliant as a GitHub Action or Docker with S3-compatible storage support.
Make sure that you have an S3-compatible storage available. We currently tested with:
The GitHub Action supports both Linux (x64 and arm64) and macOS (x64 and arm64 via a universal binary) runners. Here is how to use it:
In your workflow files, add the following global environment variables:
env:
TURBO_API: "http://127.0.0.1:8585"
TURBO_TEAM: "NAME_OF_YOUR_REPO_HERE"
# TURBO_TOKEN is required by Turborepo to enable remote caching.
# The cache server only validates it when TURBO_TOKEN is also set
# on the server itself — otherwise the server accepts any value.
# See the "Authentication" section below for details.
TURBO_TOKEN: "secret-turbo-token"
In the same workflow file, after checking out your code, start the Turbo Cache Server in the background:
- name: Checkout repository
uses: actions/checkout@v4
- name: Turborepo Cache Server
# ALWAYS use a pinned version of the action
# As we don't ship the latest versions of the binary on the main branch
# PLEASE see the latest versions here:
# https://github.com/brunojppb/turbo-cache-server/releases
uses: brunojppb/turbo-cache-server@4.0.3
env:
PORT: "8585"
S3_BUCKET_NAME: your-bucket-name-here
# Region defaults to "eu-central-1"
S3_REGION: "eu-central-1"
# Optional: If you need to provide specific auth keys, separate from default AWS credentials
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
# Optional: If not using AWS, provide endpoint like `https://rustfs` for your instance.
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
# Optional: If your S3-compatible store does not support requests
# like https://bucket.hostname.domain/. Setting `S3_USE_PATH_STYLE`
# to true configures the S3 client to make requests like
# https://hostname.domain/bucket instead.
# Defaults to "false"
S3_USE_PATH_STYLE: false
# Optional: Enable server-side encryption for stored artifacts.
# Valid values: AES256, aws:kms, aws:kms:dsse, aws:fsx
S3_SERVER_SIDE_ENCRYPTION: "AES256"
# Now you can run your turborepo tasks and rely on the cache server
# available in the background to provide previously built artifacts (cache hits)
# and let Turborepo upload new artifacts when there is a cache miss.
- name: Run tasks
run: turbo run test build typecheck
```
And that is all you need to use our remote cache server for Turborepo. As a reference, take a look at this example workflow file for inspiration.
[!NOTE] These environment variables are required by Turborepo so it can call the Turbo Cache Server with the right HTTP body, headers and query strings. These environment variables are necessary so the Turborepo binary can identify the Remote Cache feature is enabled and can use them across all steps. You can read more about this here on the Turborepo official docs.
For folks using Gitlab or any other CI environment that supports Docker, you can run the Turbo Cache Server as a docker container:
docker run \
-e S3_ACCESS_KEY=KEY \
-e S3_SECRET_KEY=SECRET \
-e S3_BUCKET_NAME=my_cache_bucket \
-e S3_ENDPOINT=https://s3_endpoint_here \
-e S3_REGION=eu \
-e S3_SERVER_SIDE_ENCRYPTION=AES256 \
# Optional: enables authentication. See "Authentication" below.
-e TURBO_TOKEN=secret-turbo-token \
-p "8000:8000" \
ghcr.io/brunojppb/turbo-cache-server:latest
Turbo Cache Server runs without authentication by default. This is an intentional design decision: in the vast majority of deployments the server sits behind a private network (a VPC, a Kubernetes cluster, or a GitHub Actions runner) where only trusted sources can reach it, and requiring a shared token adds overhead without a meaningful security benefit.
To enable authentication, set the TURBO_TOKEN environment variable on the
server. When set, every incoming request must include an
Authorization: Bearer <TURBO_TOKEN> header or it will be rejected with
401 Unauthorized. Turborepo clients read their own TURBO_TOKEN env var
and send this header automatically, so the server-side and client-side
values must match.
The server reads the Bearer scheme in any case, as HTTP requires. It
compares the token itself in constant time, so the time a rejection takes
does not tell an attacker how much of a guess was correct. A rejected
request returns a JSON body that says whether the header was missing or the
token was wrong:
{ "error": "Invalid TURBO_TOKEN" }
When TURBO_TOKEN is unset on the server, the authentication middleware is
bypassed entirely and any Authorization header on incoming requests is
ignored.
[!TIP] If you expose the cache server to the public internet, or to networks you do not fully control, you should set
TURBO_TOKENon the server.
Turbo Cache Server is a cloud-native, stateless application that runs seamlessly in Kubernetes environments. This makes it ideal for horizontally scaled deployments across multiple pods.
kubectl configured to access your clusterBelow is a minimal setup to deploy Turbo Cache Server to your Kubernetes cluster:
apiVersion: v1
kind: Secret
metadata:
name: turbo-cache-s3-credentials
namespace: default
type: Opaque
stringData:
S3_ACCESS_KEY: "your-access-key-here"
S3_SECRET_KEY: "your-secret-key-here"
# Optional: omit to run without authentication. See "Authentication" above.
TURBO_TOKEN: "secret-turbo-token"
Apply the secret:
kubectl apply -f turbo-cache-secret.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: turbo-cache-server
namespace: default
spec:
replicas: 2 # Scale horizontally as needed
selector:
matchLabels:
app: turbo-cache-server
template:
metadata:
labels:
app: turbo-cache-server
spec:
containers:
- name: turbo-cache-server
image: ghcr.io/brunojppb/turbo-cache-server:latest
ports:
- containerPort: 8000
name: http
env:
- name: PORT
value: "8000"
- name: S3_BUCKET_NAME
value: "your-bucket-name-here"
- name: S3_REGION
value: "eu-central-1"
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: S3_ACCESS_KEY
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: S3_SECRET_KEY
# Optional: only needed when authentication is enabled.
- name: TURBO_TOKEN
valueFrom:
secretKeyRef:
name: turbo-cache-s3-credentials
key: TURBO_TOKEN
- name: S3_ENDPOINT
value: "https://your-s3-endpoint.com"
- name: S3_SERVER_SIDE_ENCRYPTION
value: "AES256"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /management/health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /management/health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
Apply the deployment:
kubectl apply -f turbo-cache-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: turbo-cache-server
namespace: default
spec:
selector:
app: turbo-cache-server
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
Apply the service:
kubectl apply -f turbo-cache-service.yaml
Artifacts are uploaded to S3 using streaming rather than full in-memory buffering. There is no server-side setting for enforcing payload size, so size limits should be enforced by your reverse proxy, ingress, load balancer, or storage policy if you need them.
If you need to expose the service externally:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: turbo-cache-ingress
namespace: default
annotations:
# Configure based on your ingress controller
# nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: turbo.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: turbo-cache-server
port:
number: 8000
Apply the ingress:
kubectl apply -f turbo-cache-ingress.yaml
Once deployed, configure your Turborepo clients to use the Kubernetes service:
export TURBO_API="http://turbo-cache-server.default.svc.cluster.local:8000"
export TURBO_TEAM="your-team-name"
export TURBO_TOKEN="secret-turbo-token"
For external access through ingress:
export TURBO_API="https://turbo.yourdomain.com"
export TURBO_TEAM="your-team-name"
export TURBO_TOKEN="secret-turbo-token"
As your cache grows over time, you may want to automatically expire old cache entries to control storage usage and costs. Since Turbo Cache Server uses S3-compatible storage, you can configure bucket lifecycle rules to automatically delete objects after a specified period.
[!NOTE] Lifecycle rules are configured at the S3 bucket level, not within the Turbo Cache Server itself. This allows you to manage storage independently of the cache server configuration.
Object expiration is based on the last modified time of objects in your bucket. You can configure expiration in two ways:
For AWS S3, Cloudflare R2, RustFS, and other S3-compatible providers, you can use the AWS CLI to configure lifecycle rules.
Create a JSON file named lifecycle.json with the following content:
{
"Rules": [
{
"Status": "Enabled",
"Expiration": {
"Days": 30
}
}
]
}
Then apply the lifecycle configuration to your bucket:
aws s3api put-bucket-lifecycle-configuration \
--bucket your-bucket-name \
--lifecycle-configuration file://lifecycle.json
You can also set a specific expiration date:
{
"Rules": [
{
"Status": "Enabled",
"Expiration": {
"Date": "2025-12-31T00:00:00Z"
}
}
]
}
Turbo Cache Server includes built-in support for OpenTelemetry, providing distributed tracing and metrics collection out of the box. This enables you to monitor your cache server's performance and troubleshoot issues using industry-standard observability tools.
By default, all traces and metrics are tagged with the service name decay (the internal Rust crate name). You'll see this identifier in your observability platform when filtering or querying telemetry data. To use a different identifier, set the OTEL_SERVICE_NAME environment variable:
export OTEL_SERVICE_NAME="turbo-cache-server"
The OpenTelemetry integration works with all major observability SaaS platforms and open-source tools:
If you don't need telemetry, you can disable the OpenTelemetry SDK entirely by setting the OTEL_SDK_DISABLED environment variable to true. This follows the OpenTelemetry specification for disabling the SDK.
export OTEL_SDK_DISABLED="true"
When disabled, no OTLP exporters or system metric collectors will be initialized, and no connections will be attempted to any collector endpoint. Standard console and file logging will continue to work normally.
To enable OpenTelemetry export, set the following environment variables:
# OTLP endpoint (gRPC by default)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
# Or use HTTP protocol
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
# Optional: override the service name reported to your
# observability platform (defaults to "decay")
export OTEL_SERVICE_NAME="turbo-cache-server"
For platform-specific configurations:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.datadoghq.com"
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=<your-api-key>"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.honeycomb.io"
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=<your-api-key>"
For local testing, you can use the provided Docker Compose setup that includes RustFS, Jaeger, and Prometheus:
docker-compose -f docker-compose.otel.yml up
This starts:
Then run your cache server with:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
cargo run
Visit Jaeger to see distributed traces and Prometheus to query metrics from your local cache server.
Turbo Cache Server is a tiny web server written in Rust that uses any S3-compatible bucket as its storage layer for the artifacts generated by Turborepo.
Here is a diagram showing how the Turbo Cache Server works within our actions during a cache hit:
sequenceDiagram
actor A as Developer
participant B as GitHub
participant C as GitHub Actions
participant D as Turbo Cache Server
participant E as S3 bucket
A->>+B: Push new commit to GH.<br>Trigger PR Checks.
B->>+C: Trigger CI pipeline
C->>+D: turborepo cache server via<br/>"use: turbo-cache-server@4.0.3" action
Note right of C: Starts a server instance<br/> in the background.
D-->>-C: Turbo cache server ready
C->>+D: Turborepo executes task<br/>(e.g. test, build)
Note right of C: Cache check on the Turbo cache server<br/>for task hash "1wa2dr3"
D->>+E: Get object with name "1wa2dr3"
E-->>-D: object "1wa2dr3" exists
D-->>-C: Cache hit for task "1wa2dr3"
Note right of C: Replay logs and artifacts<br/>for task
C->>+D: Post-action: Shutdown Turbo Cache Server
D-->>-C: Turbo Cache server terminates safely
C-->>-B: CI pipline complete
B-->>-A: PR Checks done
When a cache isn't yet available, the Turbo Cache Server will handle new uploads and store the artifacts in S3 as you can see in the following diagram:
sequenceDiagram
actor A as Developer
participant B as GitHub
participant C as GitHub Actions
participant D as Turbo Cache Server
participant E as S3 bucket
A->>+B: Push new commit to GH.<br>Trigger PR Checks.
B->>+C: Trigger CI pipeline
C->>+D: turborepo cache server via<br/>"use: turbo-cache-server@4.0.3" action
Note right of C: Starts a server instance<br/> in the background.
D-->>-C: Turborepo cache server ready
C->>+D: Turborepo executes build task
Note right of C: Cache check on the server<br/>for task hash "1wa2dr3"
D->>+E: Get object with name "1wa2dr3"
E-->>-D: object "1wa2dr3" DOES NOT exist
D-->>-C: Cache miss for task "1wa2dr3"
Note right of C: Turborepo executes task normaly
C-->>C: Turborepo executes build task
C->>+D: Turborepo uploads cache artifact<br/>with hash "1wa2dr3"
D->>+E: Put object with name "1wa2dr3"
E->>-D: Object stored
D-->>-C: Cache upload complete
C->>+D: Post-action: Turbo Cache Server shutdown
D-->>-C: Turbo Cache server terminates safely
C-->>-B: CI pipline complete
B-->>-A: PR Checks done
Turbo Cache Server requires Rust 1.75 or above. To setup your environment, use the rustup script as recommended by the Rust docs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Now run the following command to run the web server locally:
cargo run
During local development, you might want to try the Turbo Dev Server locally against a JS monorepo. As it depends on a S3-compatible service for storing Turborepo artifacts, we recommend using RustFS with Docker with the following command:
docker run -d \
--name rustfs_container \
-p 9000:9000 \
-p 9001:9001 \
-v $(pwd)/s3_data:/data \
-v $(pwd)/s3_logs:/logs \
-e RUSTFS_ACCESS_KEY=rustfsadmin \
-e RUSTFS_SECRET_KEY=rustfsadmin \
-e RUSTFS_CONSOLE_ENABLE=true \
rustfs/rustfs:latest \
--address :9000 \
--console-enable \
--access-key rustfsadmin \
--secret-key rustfsadmin \
/data
Copy the .env.example file, rename it to .env and add the environment
variables required. As we use RustFS locally, open the
Web UI, create a bucket, and use rustfsadmin for
both S3_ACCESS_KEY and S3_SECRET_KEY in the .env file.
To execute the test suite, run:
cargo test
While running our end-to-end tests, you might run into the following error:
thread 'actix-server worker 9' panicked at /src/index.crates.io-6f17d22bba15001f/actix-server-2.4.0/src/worker.rs:404:34:
called `Result::unwrap()` on an `Err` value: Os { code: 24, kind: Uncategorized, message: "Too many open files" }
thread 'artifacts::list_team_artifacts_test' panicked at tests/e2e/artifacts.rs:81:29:
Failed to request /v8/artifacts
This is likely due the the maximum number of open file descriptors defined for your user. Just run the following command to fix it:
ulimit -n 1024
Rust
88.1%
JavaScript
9.5%
Dockerfile
2.3%