diff --git a/examples/go/warm_pools/main.go b/examples/go/warm_pools/main.go
new file mode 100644
index 000000000..fc4bd2e05
--- /dev/null
+++ b/examples/go/warm_pools/main.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/daytona/clients/sdk-go/pkg/daytona"
+ "github.com/daytona/clients/sdk-go/pkg/types"
+)
+
+func main() {
+ // Create a new Daytona client using environment variables.
+ // Set DAYTONA_API_KEY before running.
+ client, err := daytona.NewClient()
+ if err != nil {
+ log.Fatalf("Failed to create client: %v", err)
+ }
+
+ ctx := context.Background()
+
+ // Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+ // Target is optional and defaults to the organization default region.
+ pool, err := client.WarmPool.Create(ctx, &types.CreateWarmPoolParams{
+ Snapshot: "my-snapshot",
+ Pool: 3,
+ })
+ if err != nil {
+ log.Fatalf("Failed to create warm pool: %v", err)
+ }
+ log.Printf("✓ Created warm pool %s for snapshot %q in %s\n", pool.ID, pool.Snapshot, pool.Target)
+
+ // List warm pools. CurrentSize vs Pool is the status check: CurrentSize is the
+ // number of ready sandboxes; ErrorReason is set when the pool cannot be filled.
+ pools, err := client.WarmPool.List(ctx)
+ if err != nil {
+ log.Fatalf("Failed to list warm pools: %v", err)
+ }
+ for _, p := range pools {
+ status := ""
+ if p.ErrorReason != nil {
+ status = " (error: " + *p.ErrorReason + ")"
+ }
+ log.Printf("%s (%s): %d/%d ready%s\n", p.Snapshot, p.Target, p.CurrentSize, p.Pool, status)
+ }
+
+ // Grow the pool. Setting the size to 0 drains it without deleting the pool.
+ updated, err := client.WarmPool.Update(ctx, pool.ID, 5)
+ if err != nil {
+ log.Fatalf("Failed to update warm pool: %v", err)
+ }
+ log.Printf("✓ Updated desired size to %d\n", updated.Pool)
+
+ // Cleanup
+ if err := client.WarmPool.Delete(ctx, pool.ID); err != nil {
+ log.Fatalf("Failed to delete warm pool: %v", err)
+ }
+ log.Println("✓ Deleted warm pool")
+}
diff --git a/examples/java/warm-pools/build.gradle.kts b/examples/java/warm-pools/build.gradle.kts
new file mode 100644
index 000000000..c4269c2a1
--- /dev/null
+++ b/examples/java/warm-pools/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ application
+}
+
+group = "io.daytona.examples"
+version = "0.1.0"
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+}
+
+dependencies {
+ implementation("io.daytona:sdk-java")
+}
+
+application {
+ mainClass.set("io.daytona.examples.WarmPools")
+}
diff --git a/examples/java/warm-pools/settings.gradle.kts b/examples/java/warm-pools/settings.gradle.kts
new file mode 100644
index 000000000..c012af2f0
--- /dev/null
+++ b/examples/java/warm-pools/settings.gradle.kts
@@ -0,0 +1,14 @@
+rootProject.name = "warm-pools"
+
+dependencyResolutionManagement {
+ repositories {
+ mavenLocal()
+ mavenCentral()
+ }
+}
+
+includeBuild("../../../sdk-java") {
+ dependencySubstitution {
+ substitute(module("io.daytona:sdk-java")).using(project(":"))
+ }
+}
diff --git a/examples/java/warm-pools/src/main/java/io/daytona/examples/WarmPools.java b/examples/java/warm-pools/src/main/java/io/daytona/examples/WarmPools.java
new file mode 100644
index 000000000..6939d144c
--- /dev/null
+++ b/examples/java/warm-pools/src/main/java/io/daytona/examples/WarmPools.java
@@ -0,0 +1,32 @@
+package io.daytona.examples;
+
+import io.daytona.sdk.Daytona;
+import io.daytona.sdk.model.WarmPool;
+
+public class WarmPools {
+ public static void main(String[] args) {
+ try (Daytona daytona = new Daytona()) {
+ // Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+ // The target (third argument) is optional; null means the organization default region.
+ WarmPool pool = daytona.warmPool().create("my-snapshot", 3, null);
+ System.out.println("Created warm pool " + pool.getId()
+ + " for snapshot '" + pool.getSnapshot() + "' in " + pool.getTarget());
+
+ // List warm pools. currentSize vs pool is the status check: currentSize is the
+ // number of ready sandboxes; errorReason is set when the pool cannot be filled.
+ for (WarmPool p : daytona.warmPool().list()) {
+ String status = p.getErrorReason() != null ? " (error: " + p.getErrorReason() + ")" : "";
+ System.out.println(p.getSnapshot() + " (" + p.getTarget() + "): "
+ + p.getCurrentSize() + "/" + p.getPool() + " ready" + status);
+ }
+
+ // Grow the pool. Setting the size to 0 drains it without deleting the pool.
+ WarmPool updated = daytona.warmPool().update(pool.getId(), 5);
+ System.out.println("Updated desired size to " + updated.getPool());
+
+ // Cleanup
+ daytona.warmPool().delete(pool.getId());
+ System.out.println("Deleted warm pool");
+ }
+ }
+}
diff --git a/examples/python/warm-pools/_async/warm_pool.py b/examples/python/warm-pools/_async/warm_pool.py
new file mode 100644
index 000000000..3733ab00b
--- /dev/null
+++ b/examples/python/warm-pools/_async/warm_pool.py
@@ -0,0 +1,29 @@
+import asyncio
+
+from daytona import AsyncDaytona
+
+
+async def main():
+ async with AsyncDaytona() as daytona:
+ # Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+ # `target` is optional and defaults to the organization default region.
+ pool = await daytona.warm_pool.create("my-snapshot", pool=3)
+ print(f"Created warm pool {pool.id} for snapshot '{pool.snapshot}' in {pool.target}")
+
+ # List warm pools. current_size vs pool is the status check: current_size is the
+ # number of ready sandboxes; error_reason is set when the pool cannot be filled.
+ for p in await daytona.warm_pool.list():
+ status = f" (error: {p.error_reason})" if p.error_reason else ""
+ print(f"{p.snapshot} ({p.target}): {p.current_size}/{p.pool} ready{status}")
+
+ # Grow the pool. Setting pool to 0 drains it without deleting the pool.
+ updated = await daytona.warm_pool.update(pool.id, pool=5)
+ print(f"Updated desired size to {updated.pool}")
+
+ # Cleanup
+ await daytona.warm_pool.delete(pool.id)
+ print("Deleted warm pool")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/python/warm-pools/warm_pool.py b/examples/python/warm-pools/warm_pool.py
new file mode 100644
index 000000000..357e857e9
--- /dev/null
+++ b/examples/python/warm-pools/warm_pool.py
@@ -0,0 +1,28 @@
+from daytona import Daytona
+
+
+def main():
+ daytona = Daytona()
+
+ # Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+ # `target` is optional and defaults to the organization default region.
+ pool = daytona.warm_pool.create("my-snapshot", pool=3)
+ print(f"Created warm pool {pool.id} for snapshot '{pool.snapshot}' in {pool.target}")
+
+ # List warm pools. current_size vs pool is the status check: current_size is the
+ # number of ready sandboxes; error_reason is set when the pool cannot be filled.
+ for p in daytona.warm_pool.list():
+ status = f" (error: {p.error_reason})" if p.error_reason else ""
+ print(f"{p.snapshot} ({p.target}): {p.current_size}/{p.pool} ready{status}")
+
+ # Grow the pool. Setting pool to 0 drains it without deleting the pool.
+ updated = daytona.warm_pool.update(pool.id, pool=5)
+ print(f"Updated desired size to {updated.pool}")
+
+ # Cleanup
+ daytona.warm_pool.delete(pool.id)
+ print("Deleted warm pool")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/ruby/warm-pools/warm_pool.rb b/examples/ruby/warm-pools/warm_pool.rb
new file mode 100644
index 000000000..aa0cbb95e
--- /dev/null
+++ b/examples/ruby/warm-pools/warm_pool.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'daytona'
+
+daytona = Daytona::Daytona.new
+
+# Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+# `target` is optional and defaults to the organization default region.
+pool = daytona.warm_pool.create('my-snapshot', 3)
+puts "Created warm pool #{pool.id} for snapshot '#{pool.snapshot}' in #{pool.target}"
+
+# List warm pools. current_size vs pool is the status check: current_size is the
+# number of ready sandboxes; error_reason is set when the pool cannot be filled.
+daytona.warm_pool.list.each do |p|
+ status = p.error_reason ? " (error: #{p.error_reason})" : ''
+ puts "#{p.snapshot} (#{p.target}): #{p.current_size}/#{p.pool} ready#{status}"
+end
+
+# Grow the pool. Setting the size to 0 drains it without deleting the pool.
+updated = daytona.warm_pool.update(pool.id, 5)
+puts "Updated desired size to #{updated.pool}"
+
+# Cleanup
+daytona.warm_pool.delete(pool.id)
+puts 'Deleted warm pool'
diff --git a/examples/typescript/warm-pools/index.ts b/examples/typescript/warm-pools/index.ts
new file mode 100644
index 000000000..ce8c3b9ef
--- /dev/null
+++ b/examples/typescript/warm-pools/index.ts
@@ -0,0 +1,28 @@
+import { Daytona } from '@daytona/sdk'
+
+async function main() {
+ const daytona = new Daytona()
+
+ // Create a warm pool that keeps ready-to-use sandboxes for an existing snapshot.
+ // `target` is optional and defaults to the organization default region.
+ const pool = await daytona.warmPool.create({ snapshot: 'my-snapshot', pool: 3 })
+ console.log(`Created warm pool ${pool.id} for snapshot '${pool.snapshot}' in ${pool.target}`)
+
+ // List warm pools. currentSize vs pool is the status check: currentSize is the
+ // number of ready sandboxes; errorReason is set when the pool cannot be filled.
+ const pools = await daytona.warmPool.list()
+ for (const p of pools) {
+ const status = p.errorReason ? ` (error: ${p.errorReason})` : ''
+ console.log(`${p.snapshot} (${p.target}): ${p.currentSize}/${p.pool} ready${status}`)
+ }
+
+ // Grow the pool. Setting pool to 0 drains it without deleting the pool.
+ const updated = await daytona.warmPool.update(pool.id, { pool: 5 })
+ console.log(`Updated desired size to ${updated.pool}`)
+
+ // Cleanup
+ await daytona.warmPool.delete(pool.id)
+ console.log('Deleted warm pool')
+}
+
+main()
diff --git a/sdk-go/pkg/daytona/client.go b/sdk-go/pkg/daytona/client.go
index 82857f3b8..383b73982 100644
--- a/sdk-go/pkg/daytona/client.go
+++ b/sdk-go/pkg/daytona/client.go
@@ -132,6 +132,9 @@ type Client struct {
// Secret provides methods for managing organization secrets.
Secret *SecretService
+
+ // WarmPool provides methods for managing warm pools of ready sandboxes.
+ WarmPool *WarmPoolService
}
// NewClient creates a new Daytona client with default configuration.
@@ -272,6 +275,7 @@ func NewClientWithConfig(config *types.DaytonaConfig) (*Client, error) {
client.Volume = NewVolumeService(client)
client.Snapshot = NewSnapshotService(client)
client.Secret = NewSecretService(client)
+ client.WarmPool = NewWarmPoolService(client)
client.subscriptionManager = common.NewEventSubscriptionManager(nil)
token := client.apiKey
diff --git a/sdk-go/pkg/daytona/volume_test.go b/sdk-go/pkg/daytona/volume_test.go
index 5b88ca9d8..014477246 100644
--- a/sdk-go/pkg/daytona/volume_test.go
+++ b/sdk-go/pkg/daytona/volume_test.go
@@ -65,6 +65,7 @@ func createTestClientWithServer(t *testing.T, server *httptest.Server) *Client {
client.Volume = NewVolumeService(client)
client.Snapshot = NewSnapshotService(client)
client.Secret = NewSecretService(client)
+ client.WarmPool = NewWarmPoolService(client)
return client
}
diff --git a/sdk-go/pkg/daytona/warm_pool.go b/sdk-go/pkg/daytona/warm_pool.go
new file mode 100644
index 000000000..05b29f4ac
--- /dev/null
+++ b/sdk-go/pkg/daytona/warm_pool.go
@@ -0,0 +1,198 @@
+package daytona
+
+import (
+ "context"
+ "time"
+
+ apiclient "github.com/daytona/clients/api-client-go"
+ "github.com/daytona/clients/sdk-go/pkg/errors"
+ "github.com/daytona/clients/sdk-go/pkg/types"
+)
+
+// WarmPoolService provides warm pool management operations.
+//
+// WarmPoolService enables listing, creating, updating, and deleting warm
+// pools of ready-to-use sandboxes for a snapshot. A pool's CurrentSize versus
+// Pool is its status: CurrentSize is the number of ready warm sandboxes, Pool
+// is the desired number, and ErrorReason is set when the pool cannot be
+// filled. Access through [Client.WarmPool].
+//
+// Example:
+//
+// // Create a new warm pool
+// pool, err := client.WarmPool.Create(ctx, &types.CreateWarmPoolParams{
+// Snapshot: "my-snapshot",
+// Pool: 5,
+// })
+// if err != nil {
+// return err
+// }
+//
+// // List all warm pools
+// pools, err := client.WarmPool.List(ctx)
+type WarmPoolService struct {
+ client *Client
+ otel *otelState
+}
+
+// NewWarmPoolService creates a new WarmPoolService.
+//
+// This is typically called internally by the SDK when creating a [Client].
+// Users should access WarmPoolService through [Client.WarmPool] rather than
+// creating it directly.
+func NewWarmPoolService(client *Client) *WarmPoolService {
+ return &WarmPoolService{
+ client: client,
+ otel: client.Otel,
+ }
+}
+
+// List returns all warm pools in the organization.
+//
+// Example:
+//
+// pools, err := client.WarmPool.List(ctx)
+// if err != nil {
+// return err
+// }
+// for _, pool := range pools {
+// fmt.Printf("%s: %d/%d ready\n", pool.Snapshot, pool.CurrentSize, pool.Pool)
+// }
+//
+// Returns a slice of [types.WarmPool] or an error if the request fails.
+func (w *WarmPoolService) List(ctx context.Context) ([]*types.WarmPool, error) {
+ return withInstrumentation(ctx, w.otel, "WarmPool", "List", func(ctx context.Context) ([]*types.WarmPool, error) {
+ authCtx := w.client.getAuthContext(ctx)
+ warmPoolDtos, httpResp, err := w.client.apiClient.WarmPoolsAPI.ListWarmPools(authCtx).Execute()
+ if err != nil {
+ return nil, errors.ConvertAPIError(err, httpResp)
+ }
+
+ warmPools := make([]*types.WarmPool, len(warmPoolDtos))
+ for i := range warmPoolDtos {
+ warmPools[i] = warmPoolDtoToWarmPool(&warmPoolDtos[i])
+ }
+
+ return warmPools, nil
+ })
+}
+
+// Create creates a new warm pool.
+//
+// A pool for the same snapshot and region may exist only once; a duplicate
+// returns a conflict error.
+//
+// Parameters:
+// - params: Warm pool creation parameters including snapshot, desired pool
+// size, and optional target region
+//
+// Example:
+//
+// pool, err := client.WarmPool.Create(ctx, &types.CreateWarmPoolParams{
+// Snapshot: "my-snapshot",
+// Pool: 5,
+// })
+// if err != nil {
+// return err
+// }
+//
+// Returns the created [types.WarmPool] or an error.
+func (w *WarmPoolService) Create(ctx context.Context, params *types.CreateWarmPoolParams) (*types.WarmPool, error) {
+ return withInstrumentation(ctx, w.otel, "WarmPool", "Create", func(ctx context.Context) (*types.WarmPool, error) {
+ authCtx := w.client.getAuthContext(ctx)
+
+ req := apiclient.NewCreateWarmPool(params.Snapshot, float32(params.Pool))
+ if params.Target != nil {
+ req.SetTarget(*params.Target)
+ }
+
+ warmPoolDto, httpResp, err := w.client.apiClient.WarmPoolsAPI.CreateWarmPool(authCtx).CreateWarmPool(*req).Execute()
+ if err != nil {
+ return nil, errors.ConvertAPIError(err, httpResp)
+ }
+
+ return warmPoolDtoToWarmPool(warmPoolDto), nil
+ })
+}
+
+// Update sets the desired size of a warm pool.
+//
+// Parameters:
+// - warmPoolID: The warm pool ID
+// - pool: New desired number of warm sandboxes (0 drains the pool)
+//
+// Example:
+//
+// pool, err := client.WarmPool.Update(ctx, warmPoolID, 10)
+// if err != nil {
+// return err
+// }
+//
+// Returns the updated [types.WarmPool] or an error if the ID is unknown (404).
+func (w *WarmPoolService) Update(ctx context.Context, warmPoolID string, pool int) (*types.WarmPool, error) {
+ return withInstrumentation(ctx, w.otel, "WarmPool", "Update", func(ctx context.Context) (*types.WarmPool, error) {
+ authCtx := w.client.getAuthContext(ctx)
+
+ req := apiclient.NewUpdateWarmPool(float32(pool))
+ warmPoolDto, httpResp, err := w.client.apiClient.WarmPoolsAPI.UpdateWarmPool(authCtx, warmPoolID).UpdateWarmPool(*req).Execute()
+ if err != nil {
+ return nil, errors.ConvertAPIError(err, httpResp)
+ }
+
+ return warmPoolDtoToWarmPool(warmPoolDto), nil
+ })
+}
+
+// Delete permanently removes a warm pool.
+//
+// Parameters:
+// - warmPoolID: The warm pool ID
+//
+// Example:
+//
+// err := client.WarmPool.Delete(ctx, warmPoolID)
+// if err != nil {
+// return err
+// }
+//
+// Returns an error if the ID is unknown (404) or deletion fails.
+func (w *WarmPoolService) Delete(ctx context.Context, warmPoolID string) error {
+ return withInstrumentationVoid(ctx, w.otel, "WarmPool", "Delete", func(ctx context.Context) error {
+ authCtx := w.client.getAuthContext(ctx)
+ httpResp, err := w.client.apiClient.WarmPoolsAPI.DeleteWarmPool(authCtx, warmPoolID).Execute()
+ if err != nil {
+ return errors.ConvertAPIError(err, httpResp)
+ }
+
+ return nil
+ })
+}
+
+// warmPoolDtoToWarmPool converts api-client WarmPool to SDK types.WarmPool
+func warmPoolDtoToWarmPool(dto *apiclient.WarmPool) *types.WarmPool {
+ createdAt, _ := time.Parse(time.RFC3339, dto.GetCreatedAt())
+ updatedAt, _ := time.Parse(time.RFC3339, dto.GetUpdatedAt())
+
+ warmPool := &types.WarmPool{
+ ID: dto.GetId(),
+ OrganizationID: dto.GetOrganizationId(),
+ Snapshot: dto.GetSnapshot(),
+ Target: dto.GetTarget(),
+ Pool: int(dto.GetPool()),
+ CurrentSize: int(dto.GetCurrentSize()),
+ CPU: int(dto.GetCpu()),
+ Mem: int(dto.GetMem()),
+ Disk: int(dto.GetDisk()),
+ OsUser: dto.GetOsUser(),
+ Env: dto.GetEnv(),
+ CreatedAt: createdAt,
+ UpdatedAt: updatedAt,
+ }
+
+ // Handle nullable ErrorReason
+ if dto.ErrorReason.IsSet() {
+ warmPool.ErrorReason = dto.ErrorReason.Get()
+ }
+
+ return warmPool
+}
diff --git a/sdk-go/pkg/daytona/warm_pool_test.go b/sdk-go/pkg/daytona/warm_pool_test.go
new file mode 100644
index 000000000..4876264ed
--- /dev/null
+++ b/sdk-go/pkg/daytona/warm_pool_test.go
@@ -0,0 +1,129 @@
+package daytona
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ apiclient "github.com/daytona/clients/api-client-go"
+ "github.com/daytona/clients/sdk-go/pkg/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestWarmPoolServiceCreation(t *testing.T) {
+ t.Setenv("DAYTONA_API_KEY", "test-api-key")
+ t.Setenv("DAYTONA_API_URL", "")
+ t.Setenv("DAYTONA_JWT_TOKEN", "")
+ t.Setenv("DAYTONA_ORGANIZATION_ID", "")
+
+ client, err := NewClient()
+ require.NoError(t, err)
+
+ ws := NewWarmPoolService(client)
+ require.NotNil(t, ws)
+}
+
+func TestWarmPoolList(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode([]map[string]any{{
+ "id": "wp-1",
+ "organizationId": "org-1",
+ "snapshot": "my-snapshot",
+ "target": "us",
+ "pool": 5,
+ "currentSize": 3,
+ "cpu": 2,
+ "mem": 4,
+ "disk": 10,
+ "osUser": "daytona",
+ "env": map[string]string{},
+ "createdAt": "2025-01-01T00:00:00Z",
+ "updatedAt": "2025-01-02T00:00:00Z",
+ }})
+ }))
+ defer server.Close()
+
+ client := createTestClientWithServer(t, server)
+
+ pools, err := client.WarmPool.List(context.Background())
+ require.NoError(t, err)
+ require.Len(t, pools, 1)
+ assert.Equal(t, "wp-1", pools[0].ID)
+ assert.Equal(t, "my-snapshot", pools[0].Snapshot)
+ assert.Equal(t, 5, pools[0].Pool)
+ assert.Equal(t, 3, pools[0].CurrentSize)
+}
+
+func TestWarmPoolListError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ _ = json.NewEncoder(w).Encode(map[string]string{"message": "internal error"})
+ }))
+ defer server.Close()
+
+ client := createTestClientWithServer(t, server)
+
+ _, err := client.WarmPool.List(context.Background())
+ require.Error(t, err)
+}
+
+func TestWarmPoolCreateError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusConflict)
+ _ = json.NewEncoder(w).Encode(map[string]string{"message": "warm pool already exists"})
+ }))
+ defer server.Close()
+
+ client := createTestClientWithServer(t, server)
+
+ _, err := client.WarmPool.Create(context.Background(), &types.CreateWarmPoolParams{
+ Snapshot: "my-snapshot",
+ Pool: 5,
+ })
+ require.Error(t, err)
+}
+
+func TestWarmPoolDeleteError(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusNotFound)
+ _ = json.NewEncoder(w).Encode(map[string]string{"message": "not found"})
+ }))
+ defer server.Close()
+
+ client := createTestClientWithServer(t, server)
+
+ err := client.WarmPool.Delete(context.Background(), "wp-1")
+ require.Error(t, err)
+}
+
+func TestWarmPoolDtoToWarmPool(t *testing.T) {
+ dto := apiclient.NewWarmPool(
+ "wp-1", "org-1", "my-snapshot", "us", 5, 3, 2, 4, 10, "daytona",
+ map[string]string{"FOO": "bar"}, "2025-01-01T00:00:00Z", "2025-01-02T00:00:00Z",
+ )
+ dto.SetErrorReason("quota exceeded")
+
+ warmPool := warmPoolDtoToWarmPool(dto)
+
+ assert.Equal(t, "wp-1", warmPool.ID)
+ assert.Equal(t, "org-1", warmPool.OrganizationID)
+ assert.Equal(t, "my-snapshot", warmPool.Snapshot)
+ assert.Equal(t, "us", warmPool.Target)
+ assert.Equal(t, 5, warmPool.Pool)
+ assert.Equal(t, 3, warmPool.CurrentSize)
+ assert.Equal(t, 2, warmPool.CPU)
+ assert.Equal(t, 4, warmPool.Mem)
+ assert.Equal(t, 10, warmPool.Disk)
+ assert.Equal(t, "daytona", warmPool.OsUser)
+ assert.Equal(t, map[string]string{"FOO": "bar"}, warmPool.Env)
+ require.NotNil(t, warmPool.ErrorReason)
+ assert.Equal(t, "quota exceeded", *warmPool.ErrorReason)
+ assert.Equal(t, "2025-01-01T00:00:00Z", warmPool.CreatedAt.Format("2006-01-02T15:04:05Z"))
+}
diff --git a/sdk-go/pkg/types/types.go b/sdk-go/pkg/types/types.go
index 855b8b2e9..035df2c87 100644
--- a/sdk-go/pkg/types/types.go
+++ b/sdk-go/pkg/types/types.go
@@ -236,6 +236,44 @@ type ListSecretsResponse struct {
NextCursor *string
}
+// WarmPool represents a warm pool of ready-to-use sandboxes for a snapshot.
+//
+// CurrentSize versus Pool is the pool's status: CurrentSize is the number of
+// ready warm sandboxes, Pool is the desired number. ErrorReason is set when
+// the pool cannot be filled.
+type WarmPool struct {
+ ID string `json:"id"`
+ OrganizationID string `json:"organizationId"`
+ // Snapshot is the snapshot the pool keeps warm sandboxes for.
+ Snapshot string `json:"snapshot"`
+ // Target is the target region of the pool.
+ Target string `json:"target"`
+ // Pool is the desired number of warm sandboxes.
+ Pool int `json:"pool"`
+ // CurrentSize is the current number of ready warm sandboxes in the pool.
+ CurrentSize int `json:"currentSize"`
+ CPU int `json:"cpu"`
+ Mem int `json:"mem"`
+ Disk int `json:"disk"`
+ OsUser string `json:"osUser"`
+ Env map[string]string `json:"env"`
+ // ErrorReason is set when the pool cannot be filled.
+ ErrorReason *string `json:"errorReason,omitempty"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+// CreateWarmPoolParams contains parameters for creating a warm pool.
+type CreateWarmPoolParams struct {
+ // Snapshot is the snapshot (ID or name) to keep warm sandboxes for.
+ Snapshot string
+ // Pool is the number of warm sandboxes to keep ready.
+ Pool int
+ // Target is the target region for the warm pool. Defaults to the
+ // organization default region when nil.
+ Target *string
+}
+
// Snapshot represents a Daytona snapshot
type Snapshot struct {
ID string `json:"id"`
diff --git a/sdk-java/src/main/java/io/daytona/sdk/Daytona.java b/sdk-java/src/main/java/io/daytona/sdk/Daytona.java
index bb6d1a407..7ada667ba 100644
--- a/sdk-java/src/main/java/io/daytona/sdk/Daytona.java
+++ b/sdk-java/src/main/java/io/daytona/sdk/Daytona.java
@@ -54,6 +54,7 @@ public class Daytona implements AutoCloseable {
private final SnapshotService snapshot;
private final VolumeService volume;
private final SecretService secret;
+ private final WarmPoolService warmPool;
private final EventDispatcher eventDispatcher;
private final EventSubscriptionManager subscriptionManager;
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -93,6 +94,7 @@ public Daytona(DaytonaConfig config) {
this.snapshot = new SnapshotService(new io.daytona.api.client.api.SnapshotsApi(apiClient), apiClient.getHttpClient(), config.getApiKey());
this.volume = new VolumeService(new io.daytona.api.client.api.VolumesApi(apiClient));
this.secret = new SecretService(new io.daytona.api.client.api.SecretApi(apiClient));
+ this.warmPool = new WarmPoolService(new io.daytona.api.client.api.WarmPoolsApi(apiClient));
@SuppressWarnings("deprecation")
boolean legacyPolling = config.isUseDeprecatedPolling();
if (!legacyPolling) {
@@ -535,6 +537,15 @@ public SecretService secret() {
return secret;
}
+ /**
+ * Returns Warm Pool management service.
+ *
+ * @return warm pool service instance
+ */
+ public WarmPoolService warmPool() {
+ return warmPool;
+ }
+
/**
* Closes this client and releases underlying HTTP resources.
*/
diff --git a/sdk-java/src/main/java/io/daytona/sdk/WarmPoolService.java b/sdk-java/src/main/java/io/daytona/sdk/WarmPoolService.java
new file mode 100644
index 000000000..9f2d3fece
--- /dev/null
+++ b/sdk-java/src/main/java/io/daytona/sdk/WarmPoolService.java
@@ -0,0 +1,102 @@
+package io.daytona.sdk;
+
+import io.daytona.api.client.api.WarmPoolsApi;
+import io.daytona.api.client.model.CreateWarmPool;
+import io.daytona.api.client.model.UpdateWarmPool;
+import io.daytona.sdk.model.WarmPool;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Service for managing Daytona Warm Pools.
+ *
+ *
Warm pools keep ready-to-use Sandboxes for a snapshot. A pool's {@code currentSize} versus
+ * {@code pool} is its status: {@code currentSize} is the number of ready warm sandboxes,
+ * {@code pool} is the desired number, and {@code errorReason} is set when the pool cannot be
+ * filled.
+ */
+public class WarmPoolService {
+ private final WarmPoolsApi warmPoolsApi;
+
+ WarmPoolService(WarmPoolsApi warmPoolsApi) {
+ this.warmPoolsApi = warmPoolsApi;
+ }
+
+ /**
+ * Lists all warm pools in the organization.
+ *
+ * @return list of warm pools
+ * @throws io.daytona.sdk.exception.DaytonaException if the API request fails
+ */
+ public List list() {
+ List warmPools = ExceptionMapper.callMain(() -> warmPoolsApi.listWarmPools(null));
+ List result = new ArrayList();
+ if (warmPools != null) {
+ for (io.daytona.api.client.model.WarmPool warmPool : warmPools) {
+ result.add(toWarmPool(warmPool));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new warm pool.
+ *
+ * @param snapshot snapshot (ID or name) to keep warm sandboxes for
+ * @param pool number of warm sandboxes to keep ready
+ * @param target target region, or {@code null} for the organization default region
+ * @return created {@link WarmPool}
+ * @throws io.daytona.sdk.exception.DaytonaException if a pool for the same snapshot and
+ * region already exists or creation fails
+ */
+ public WarmPool create(String snapshot, int pool, String target) {
+ CreateWarmPool request = new CreateWarmPool().snapshot(snapshot).pool(BigDecimal.valueOf(pool));
+ if (target != null) {
+ request.target(target);
+ }
+ io.daytona.api.client.model.WarmPool warmPoolDto = ExceptionMapper.callMain(
+ () -> warmPoolsApi.createWarmPool(request, null)
+ );
+ return toWarmPool(warmPoolDto);
+ }
+
+ /**
+ * Updates the desired size of a warm pool.
+ *
+ * @param id warm pool identifier
+ * @param pool new desired number of warm sandboxes (0 drains the pool)
+ * @return updated {@link WarmPool}
+ * @throws io.daytona.sdk.exception.DaytonaException if no warm pool is found or request fails
+ */
+ public WarmPool update(String id, int pool) {
+ io.daytona.api.client.model.WarmPool warmPoolDto = ExceptionMapper.callMain(
+ () -> warmPoolsApi.updateWarmPool(id, new UpdateWarmPool().pool(BigDecimal.valueOf(pool)), null)
+ );
+ return toWarmPool(warmPoolDto);
+ }
+
+ /**
+ * Deletes a warm pool by ID.
+ *
+ * @param id warm pool identifier
+ * @throws io.daytona.sdk.exception.DaytonaException if deletion fails
+ */
+ public void delete(String id) {
+ ExceptionMapper.runMain(() -> warmPoolsApi.deleteWarmPool(id, null));
+ }
+
+ private WarmPool toWarmPool(io.daytona.api.client.model.WarmPool source) {
+ WarmPool warmPool = new WarmPool();
+ if (source != null) {
+ warmPool.setId(source.getId());
+ warmPool.setSnapshot(source.getSnapshot());
+ warmPool.setTarget(source.getTarget());
+ warmPool.setPool(source.getPool() == null ? 0 : source.getPool().intValue());
+ warmPool.setCurrentSize(source.getCurrentSize() == null ? 0 : source.getCurrentSize().intValue());
+ warmPool.setErrorReason(source.getErrorReason());
+ }
+ return warmPool;
+ }
+}
diff --git a/sdk-java/src/main/java/io/daytona/sdk/model/WarmPool.java b/sdk-java/src/main/java/io/daytona/sdk/model/WarmPool.java
new file mode 100644
index 000000000..b59bc0137
--- /dev/null
+++ b/sdk-java/src/main/java/io/daytona/sdk/model/WarmPool.java
@@ -0,0 +1,111 @@
+package io.daytona.sdk.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+/**
+ * Warm pool metadata returned by Daytona APIs.
+ *
+ * {@code currentSize} versus {@code pool} is the pool's status: {@code currentSize} is the
+ * number of ready warm sandboxes, {@code pool} is the desired number. {@code errorReason} is
+ * set when the pool cannot be filled.
+ */
+public class WarmPool {
+ @JsonProperty("id")
+ private String id;
+ @JsonProperty("snapshot")
+ private String snapshot;
+ @JsonProperty("target")
+ private String target;
+ @JsonProperty("pool")
+ private int pool;
+ @JsonProperty("currentSize")
+ private int currentSize;
+ @JsonProperty("errorReason")
+ private String errorReason;
+
+ /**
+ * Returns warm pool identifier.
+ *
+ * @return warm pool ID
+ */
+ public String getId() { return id; }
+
+ /**
+ * Sets warm pool identifier.
+ *
+ * @param id warm pool ID
+ */
+ public void setId(String id) { this.id = id; }
+
+ /**
+ * Returns the snapshot the pool keeps warm sandboxes for.
+ *
+ * @return snapshot ID or name
+ */
+ public String getSnapshot() { return snapshot; }
+
+ /**
+ * Sets the snapshot the pool keeps warm sandboxes for.
+ *
+ * @param snapshot snapshot ID or name
+ */
+ public void setSnapshot(String snapshot) { this.snapshot = snapshot; }
+
+ /**
+ * Returns the target region of the pool.
+ *
+ * @return target region
+ */
+ public String getTarget() { return target; }
+
+ /**
+ * Sets the target region of the pool.
+ *
+ * @param target target region
+ */
+ public void setTarget(String target) { this.target = target; }
+
+ /**
+ * Returns the desired number of warm sandboxes.
+ *
+ * @return desired pool size
+ */
+ public int getPool() { return pool; }
+
+ /**
+ * Sets the desired number of warm sandboxes.
+ *
+ * @param pool desired pool size
+ */
+ public void setPool(int pool) { this.pool = pool; }
+
+ /**
+ * Returns the current number of ready warm sandboxes in the pool.
+ *
+ * @return current pool size
+ */
+ public int getCurrentSize() { return currentSize; }
+
+ /**
+ * Sets the current number of ready warm sandboxes in the pool.
+ *
+ * @param currentSize current pool size
+ */
+ public void setCurrentSize(int currentSize) { this.currentSize = currentSize; }
+
+ /**
+ * Returns the reason the pool cannot be filled, if any.
+ *
+ * @return error reason or {@code null}
+ */
+ public String getErrorReason() { return errorReason; }
+
+ /**
+ * Sets the reason the pool cannot be filled.
+ *
+ * @param errorReason error reason
+ */
+ public void setErrorReason(String errorReason) { this.errorReason = errorReason; }
+}
diff --git a/sdk-java/src/test/java/io/daytona/sdk/WarmPoolServiceTest.java b/sdk-java/src/test/java/io/daytona/sdk/WarmPoolServiceTest.java
new file mode 100644
index 000000000..8daf60483
--- /dev/null
+++ b/sdk-java/src/test/java/io/daytona/sdk/WarmPoolServiceTest.java
@@ -0,0 +1,140 @@
+package io.daytona.sdk;
+
+import io.daytona.api.client.api.WarmPoolsApi;
+import io.daytona.api.client.model.CreateWarmPool;
+import io.daytona.api.client.model.UpdateWarmPool;
+import io.daytona.sdk.exception.DaytonaNotFoundException;
+import io.daytona.sdk.exception.DaytonaServerException;
+import io.daytona.sdk.model.WarmPool;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class WarmPoolServiceTest {
+
+ @Mock
+ private WarmPoolsApi warmPoolsApi;
+
+ private WarmPoolService service;
+
+ @BeforeEach
+ void setUp() {
+ service = new WarmPoolService(warmPoolsApi);
+ }
+
+ @Test
+ void createMapsResponse() {
+ when(warmPoolsApi.createWarmPool(any(), isNull())).thenReturn(warmPoolDto("wp-1", "snap", 5, 3));
+
+ WarmPool warmPool = service.create("snap", 5, null);
+
+ assertThat(warmPool.getId()).isEqualTo("wp-1");
+ assertThat(warmPool.getSnapshot()).isEqualTo("snap");
+ assertThat(warmPool.getPool()).isEqualTo(5);
+ assertThat(warmPool.getCurrentSize()).isEqualTo(3);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(CreateWarmPool.class);
+ verify(warmPoolsApi).createWarmPool(captor.capture(), isNull());
+ assertThat(captor.getValue().getSnapshot()).isEqualTo("snap");
+ assertThat(captor.getValue().getPool()).isEqualTo(BigDecimal.valueOf(5));
+ assertThat(captor.getValue().getTarget()).isNull();
+ }
+
+ @Test
+ void createPassesTargetWhenGiven() {
+ when(warmPoolsApi.createWarmPool(any(), isNull())).thenReturn(warmPoolDto("wp-1", "snap", 5, 0));
+
+ service.create("snap", 5, "eu");
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(CreateWarmPool.class);
+ verify(warmPoolsApi).createWarmPool(captor.capture(), isNull());
+ assertThat(captor.getValue().getTarget()).isEqualTo("eu");
+ }
+
+ @Test
+ void listMapsAllItems() {
+ when(warmPoolsApi.listWarmPools(isNull())).thenReturn(Arrays.asList(
+ warmPoolDto("wp-1", "snap-a", 5, 5),
+ warmPoolDto("wp-2", "snap-b", 3, 1)
+ ));
+
+ assertThat(service.list())
+ .extracting(WarmPool::getSnapshot)
+ .containsExactly("snap-a", "snap-b");
+ }
+
+ @Test
+ void listReturnsEmptyListWhenApiReturnsNull() {
+ when(warmPoolsApi.listWarmPools(isNull())).thenReturn(null);
+
+ assertThat(service.list()).isEqualTo(Collections.emptyList());
+ }
+
+ @Test
+ void updateMapsResponse() {
+ when(warmPoolsApi.updateWarmPool(eq("wp-1"), any(), isNull())).thenReturn(warmPoolDto("wp-1", "snap", 10, 3));
+
+ WarmPool warmPool = service.update("wp-1", 10);
+
+ assertThat(warmPool.getPool()).isEqualTo(10);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UpdateWarmPool.class);
+ verify(warmPoolsApi).updateWarmPool(eq("wp-1"), captor.capture(), isNull());
+ assertThat(captor.getValue().getPool()).isEqualTo(BigDecimal.valueOf(10));
+ }
+
+ @Test
+ void deleteDelegatesToApi() {
+ service.delete("wp-1");
+
+ verify(warmPoolsApi).deleteWarmPool("wp-1", null);
+ }
+
+ @ParameterizedTest
+ @MethodSource("mappedMainApiExceptions")
+ void updateMapsApiErrors(int status, Class extends RuntimeException> type) {
+ when(warmPoolsApi.updateWarmPool(eq("wp-1"), any(), isNull()))
+ .thenThrow(new io.daytona.api.client.ApiException(status, "boom", null, "{\"message\":\"mapped\"}"));
+
+ assertThatThrownBy(() -> service.update("wp-1", 1))
+ .isInstanceOf(type)
+ .hasMessage("mapped");
+ }
+
+ private static Stream mappedMainApiExceptions() {
+ return Stream.of(
+ Arguments.of(404, DaytonaNotFoundException.class),
+ Arguments.of(500, DaytonaServerException.class)
+ );
+ }
+
+ private static io.daytona.api.client.model.WarmPool warmPoolDto(String id, String snapshot, int pool, int currentSize) {
+ io.daytona.api.client.model.WarmPool dto = new io.daytona.api.client.model.WarmPool();
+ dto.setId(id);
+ dto.setSnapshot(snapshot);
+ dto.setTarget("us");
+ dto.setPool(BigDecimal.valueOf(pool));
+ dto.setCurrentSize(BigDecimal.valueOf(currentSize));
+ return dto;
+ }
+}
diff --git a/sdk-python/src/daytona/__init__.py b/sdk-python/src/daytona/__init__.py
index 87016a444..cca78a0ba 100644
--- a/sdk-python/src/daytona/__init__.py
+++ b/sdk-python/src/daytona/__init__.py
@@ -99,6 +99,7 @@
from .common.secret import CreateSecretParams, ListSecretsResponse, Secret, UpdateSecretParams
from .common.snapshot import CreateSnapshotParams
from .common.volume import VolumeMount
+ from .common.warm_pool import WarmPool
__all__ = [
"Daytona",
@@ -135,6 +136,7 @@
"CancelEvent",
"FileUpload",
"VolumeMount",
+ "WarmPool",
"Secret",
"CreateSecretParams",
"UpdateSecretParams",
@@ -307,6 +309,8 @@
"CreateSnapshotParams": "common.snapshot",
# common.volume
"VolumeMount": "common.volume",
+ # common.warm_pool
+ "WarmPool": "common.warm_pool",
# common.secret
"Secret": "common.secret",
"CreateSecretParams": "common.secret",
diff --git a/sdk-python/src/daytona/_async/daytona.py b/sdk-python/src/daytona/_async/daytona.py
index 69eaef990..e411cc6ee 100644
--- a/sdk-python/src/daytona/_async/daytona.py
+++ b/sdk-python/src/daytona/_async/daytona.py
@@ -40,6 +40,7 @@
SnapshotsApi,
)
from daytona_api_client_async import VolumesApi as VolumesApi
+from daytona_api_client_async import WarmPoolsApi
from daytona_toolbox_api_client_async import ApiClient as ToolboxApiClient
from .._utils.enum import to_enum
@@ -67,6 +68,7 @@
from .secret import AsyncSecretService
from .snapshot import AsyncSnapshotService
from .volume import AsyncVolumeService
+from .warm_pool import AsyncWarmPoolService
_MISSING_HAPPY_EYEBALLS_DELAY = object()
@@ -112,6 +114,7 @@ class AsyncDaytona:
volume (AsyncVolumeService): Service for managing volumes.
snapshot (AsyncSnapshotService): Service for managing snapshots.
secret (AsyncSecretService): Service for managing secrets.
+ warm_pool (AsyncWarmPoolService): Service for managing warm pools.
Example:
Using environment variables:
@@ -312,6 +315,7 @@ def __init__(self, config: DaytonaConfig | None = None):
self._shared_session,
)
self.secret: AsyncSecretService = AsyncSecretService(SecretApi(self._api_client))
+ self.warm_pool: AsyncWarmPoolService = AsyncWarmPoolService(WarmPoolsApi(self._api_client))
use_deprecated_polling = resolve_bool_flag(
config.use_deprecated_polling if config else None,
diff --git a/sdk-python/src/daytona/_async/warm_pool.py b/sdk-python/src/daytona/_async/warm_pool.py
new file mode 100644
index 000000000..90c3516a6
--- /dev/null
+++ b/sdk-python/src/daytona/_async/warm_pool.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from daytona_api_client_async import CreateWarmPool, UpdateWarmPool, WarmPoolsApi
+
+from .._utils.otel_decorator import with_instrumentation
+from ..common.warm_pool import WarmPool
+
+
+class AsyncWarmPoolService:
+ """Service for managing Daytona Warm Pools. Can be used to list, create, update and delete Warm Pools."""
+
+ def __init__(self, warm_pools_api: WarmPoolsApi):
+ self.__warm_pools_api = warm_pools_api
+
+ @with_instrumentation()
+ async def list(self) -> list[WarmPool]:
+ """List all Warm Pools in the organization.
+
+ Returns:
+ list[WarmPool]: List of all Warm Pools.
+
+ Example:
+ ```python
+ async with AsyncDaytona() as daytona:
+ pools = await daytona.warm_pool.list()
+ for pool in pools:
+ print(f"{pool.snapshot}: {pool.current_size}/{pool.pool} ready")
+ ```
+ """
+ return [WarmPool.from_dto(pool) for pool in await self.__warm_pools_api.list_warm_pools()]
+
+ @with_instrumentation()
+ async def create(self, snapshot: str, pool: int, target: str | None = None) -> WarmPool:
+ """Create a new Warm Pool.
+
+ Args:
+ snapshot (str): The snapshot (ID or name) to keep warm sandboxes for.
+ pool (int): Number of warm sandboxes to keep ready.
+ target (str | None): Target region for the Warm Pool. Defaults to the
+ organization default region.
+
+ Returns:
+ WarmPool: The newly created Warm Pool.
+
+ Raises:
+ ApiException: If a Warm Pool for the same snapshot and region already exists (409).
+
+ Example:
+ ```python
+ async with AsyncDaytona() as daytona:
+ pool = await daytona.warm_pool.create("my-snapshot", pool=5)
+ print(f"Created warm pool {pool.id} in {pool.target}")
+ ```
+ """
+ return WarmPool.from_dto(
+ await self.__warm_pools_api.create_warm_pool(CreateWarmPool(snapshot=snapshot, pool=pool, target=target))
+ )
+
+ @with_instrumentation()
+ async def update(self, warm_pool_id: str, pool: int) -> WarmPool:
+ """Update the desired size of a Warm Pool.
+
+ Args:
+ warm_pool_id (str): ID of the Warm Pool to update.
+ pool (int): New desired number of warm sandboxes (0 drains the pool).
+
+ Returns:
+ WarmPool: The updated Warm Pool.
+
+ Raises:
+ NotFoundException: If the Warm Pool does not exist.
+
+ Example:
+ ```python
+ async with AsyncDaytona() as daytona:
+ pool = await daytona.warm_pool.update("warm-pool-id", pool=10)
+ ```
+ """
+ return WarmPool.from_dto(await self.__warm_pools_api.update_warm_pool(warm_pool_id, UpdateWarmPool(pool=pool)))
+
+ @with_instrumentation()
+ async def delete(self, warm_pool_id: str) -> None:
+ """Delete a Warm Pool.
+
+ Args:
+ warm_pool_id (str): ID of the Warm Pool to delete.
+
+ Raises:
+ NotFoundException: If the Warm Pool does not exist.
+
+ Example:
+ ```python
+ async with AsyncDaytona() as daytona:
+ await daytona.warm_pool.delete("warm-pool-id")
+ print("Warm pool deleted")
+ ```
+ """
+ await self.__warm_pools_api.delete_warm_pool(warm_pool_id)
diff --git a/sdk-python/src/daytona/_sync/daytona.py b/sdk-python/src/daytona/_sync/daytona.py
index 89fdc5aba..35ac0fdc9 100644
--- a/sdk-python/src/daytona/_sync/daytona.py
+++ b/sdk-python/src/daytona/_sync/daytona.py
@@ -26,6 +26,7 @@
from daytona_api_client import GpuType as SyncGpuType
from daytona_api_client import ObjectStorageApi, SandboxApi, SandboxState, SandboxVolume, SecretApi, SnapshotsApi
from daytona_api_client import VolumesApi as VolumesApi
+from daytona_api_client import WarmPoolsApi
from daytona_toolbox_api_client import ApiClient as ToolboxApiClient
from .._utils.enum import to_enum
@@ -53,6 +54,7 @@
from .secret import SecretService
from .snapshot import SnapshotService
from .volume import VolumeService
+from .warm_pool import WarmPoolService
class Daytona:
@@ -65,6 +67,7 @@ class Daytona:
volume (VolumeService): Service for managing volumes.
snapshot (SnapshotService): Service for managing snapshots.
secret (SecretService): Service for managing secrets.
+ warm_pool (WarmPoolService): Service for managing warm pools.
Example:
Using environment variables:
@@ -244,6 +247,7 @@ def __init__(self, config: DaytonaConfig | None = None):
SnapshotsApi(self._api_client), self._object_storage_api, self._target
)
self.secret: SecretService = SecretService(SecretApi(self._api_client))
+ self.warm_pool: WarmPoolService = WarmPoolService(WarmPoolsApi(self._api_client))
env = env_reader or DaytonaEnvReader()
use_deprecated_polling = resolve_bool_flag(
diff --git a/sdk-python/src/daytona/_sync/warm_pool.py b/sdk-python/src/daytona/_sync/warm_pool.py
new file mode 100644
index 000000000..65c86651f
--- /dev/null
+++ b/sdk-python/src/daytona/_sync/warm_pool.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from daytona_api_client import CreateWarmPool, UpdateWarmPool, WarmPoolsApi
+
+from .._utils.otel_decorator import with_instrumentation
+from ..common.warm_pool import WarmPool
+
+
+class WarmPoolService:
+ """Service for managing Daytona Warm Pools. Can be used to list, create, update and delete Warm Pools."""
+
+ def __init__(self, warm_pools_api: WarmPoolsApi):
+ self.__warm_pools_api = warm_pools_api
+
+ @with_instrumentation()
+ def list(self) -> list[WarmPool]:
+ """List all Warm Pools in the organization.
+
+ Returns:
+ list[WarmPool]: List of all Warm Pools.
+
+ Example:
+ ```python
+ daytona = Daytona()
+ pools = daytona.warm_pool.list()
+ for pool in pools:
+ print(f"{pool.snapshot}: {pool.current_size}/{pool.pool} ready")
+ ```
+ """
+ return [WarmPool.from_dto(pool) for pool in self.__warm_pools_api.list_warm_pools()]
+
+ @with_instrumentation()
+ def create(self, snapshot: str, pool: int, target: str | None = None) -> WarmPool:
+ """Create a new Warm Pool.
+
+ Args:
+ snapshot (str): The snapshot (ID or name) to keep warm sandboxes for.
+ pool (int): Number of warm sandboxes to keep ready.
+ target (str | None): Target region for the Warm Pool. Defaults to the
+ organization default region.
+
+ Returns:
+ WarmPool: The newly created Warm Pool.
+
+ Raises:
+ ApiException: If a Warm Pool for the same snapshot and region already exists (409).
+
+ Example:
+ ```python
+ daytona = Daytona()
+ pool = daytona.warm_pool.create("my-snapshot", pool=5)
+ print(f"Created warm pool {pool.id} in {pool.target}")
+ ```
+ """
+ return WarmPool.from_dto(
+ self.__warm_pools_api.create_warm_pool(CreateWarmPool(snapshot=snapshot, pool=pool, target=target))
+ )
+
+ @with_instrumentation()
+ def update(self, warm_pool_id: str, pool: int) -> WarmPool:
+ """Update the desired size of a Warm Pool.
+
+ Args:
+ warm_pool_id (str): ID of the Warm Pool to update.
+ pool (int): New desired number of warm sandboxes (0 drains the pool).
+
+ Returns:
+ WarmPool: The updated Warm Pool.
+
+ Raises:
+ NotFoundException: If the Warm Pool does not exist.
+
+ Example:
+ ```python
+ daytona = Daytona()
+ pool = daytona.warm_pool.update("warm-pool-id", pool=10)
+ ```
+ """
+ return WarmPool.from_dto(self.__warm_pools_api.update_warm_pool(warm_pool_id, UpdateWarmPool(pool=pool)))
+
+ @with_instrumentation()
+ def delete(self, warm_pool_id: str) -> None:
+ """Delete a Warm Pool.
+
+ Args:
+ warm_pool_id (str): ID of the Warm Pool to delete.
+
+ Raises:
+ NotFoundException: If the Warm Pool does not exist.
+
+ Example:
+ ```python
+ daytona = Daytona()
+ daytona.warm_pool.delete("warm-pool-id")
+ print("Warm pool deleted")
+ ```
+ """
+ self.__warm_pools_api.delete_warm_pool(warm_pool_id)
diff --git a/sdk-python/src/daytona/common/warm_pool.py b/sdk-python/src/daytona/common/warm_pool.py
new file mode 100644
index 000000000..ef2c714e3
--- /dev/null
+++ b/sdk-python/src/daytona/common/warm_pool.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from daytona_api_client import WarmPool as WarmPoolDto
+from daytona_api_client_async import WarmPool as AsyncWarmPoolDto
+
+
+class WarmPool(WarmPoolDto):
+ """Represents a Daytona Warm Pool which keeps ready-to-use Sandboxes for a snapshot.
+
+ ``current_size`` versus ``pool`` is the pool's status: ``current_size`` is the number
+ of ready warm sandboxes, ``pool`` is the desired number. ``error_reason`` is set when
+ the pool cannot be filled.
+
+ Attributes:
+ id (str): Unique identifier for the Warm Pool.
+ organization_id (str): Organization ID that owns the Warm Pool.
+ snapshot (str): Snapshot the pool keeps warm sandboxes for.
+ target (str): Target region of the pool.
+ pool (int): Desired number of warm sandboxes.
+ current_size (int): Current number of ready warm sandboxes in the pool.
+ cpu (int): CPU cores per sandbox.
+ mem (int): Memory per sandbox in GiB.
+ disk (int): Disk per sandbox in GiB.
+ os_user (str): OS user of the warm sandboxes.
+ env (dict[str, str]): Environment variables of the warm sandboxes.
+ error_reason (str | None): Reason the pool cannot be filled, if any.
+ created_at (str): Date and time when the Warm Pool was created.
+ updated_at (str): Date and time when the Warm Pool was last updated.
+ """
+
+ @classmethod
+ def from_dto(cls, dto: WarmPoolDto | AsyncWarmPoolDto) -> "WarmPool":
+ return cls.model_validate(dto.model_dump())
diff --git a/sdk-python/tests/test_warm_pool.py b/sdk-python/tests/test_warm_pool.py
new file mode 100644
index 000000000..6b34e60cc
--- /dev/null
+++ b/sdk-python/tests/test_warm_pool.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+from daytona.common.warm_pool import WarmPool
+from daytona_api_client import WarmPool as WarmPoolDto
+
+
+def _make_warm_pool_dto(pool_id="wp-123", pool=3):
+ return WarmPoolDto(
+ id=pool_id,
+ organization_id="org-1",
+ snapshot="test-snapshot",
+ target="us",
+ pool=pool,
+ current_size=1,
+ cpu=2,
+ mem=4,
+ disk=10,
+ os_user="daytona",
+ env={},
+ error_reason=None,
+ created_at="2025-01-01T00:00:00Z",
+ updated_at="2025-01-01T00:00:00Z",
+ )
+
+
+class TestSyncWarmPoolService:
+ def _make_service(self):
+ from daytona._sync.warm_pool import WarmPoolService
+
+ mock_api = MagicMock()
+ return WarmPoolService(mock_api), mock_api
+
+ def test_list(self):
+ service, api = self._make_service()
+ api.list_warm_pools.return_value = [_make_warm_pool_dto()]
+ result = service.list()
+ assert len(result) == 1
+ assert isinstance(result[0], WarmPool)
+
+ def test_create(self):
+ service, api = self._make_service()
+ api.create_warm_pool.return_value = _make_warm_pool_dto(pool=5)
+ result = service.create("test-snapshot", pool=5)
+ assert isinstance(result, WarmPool)
+ create_dto = api.create_warm_pool.call_args.args[0]
+ assert create_dto.snapshot == "test-snapshot"
+ assert create_dto.pool == 5
+ assert create_dto.target is None
+
+ def test_update(self):
+ service, api = self._make_service()
+ api.update_warm_pool.return_value = _make_warm_pool_dto(pool=10)
+ result = service.update("wp-123", pool=10)
+ assert isinstance(result, WarmPool)
+ update_args = api.update_warm_pool.call_args.args
+ assert update_args[0] == "wp-123"
+ assert update_args[1].pool == 10
+
+ def test_delete(self):
+ service, api = self._make_service()
+ api.delete_warm_pool.return_value = None
+ service.delete("wp-123")
+ api.delete_warm_pool.assert_called_once_with("wp-123")
+
+
+class TestAsyncWarmPoolService:
+ def _make_service(self):
+ from daytona._async.warm_pool import AsyncWarmPoolService
+
+ mock_api = AsyncMock()
+ return AsyncWarmPoolService(mock_api), mock_api
+
+ @pytest.mark.asyncio
+ async def test_list(self):
+ service, api = self._make_service()
+ api.list_warm_pools.return_value = [_make_warm_pool_dto()]
+ result = await service.list()
+ assert len(result) == 1
+ assert isinstance(result[0], WarmPool)
+
+ @pytest.mark.asyncio
+ async def test_create(self):
+ service, api = self._make_service()
+ api.create_warm_pool.return_value = _make_warm_pool_dto(pool=5)
+ result = await service.create("test-snapshot", pool=5, target="us")
+ assert isinstance(result, WarmPool)
+ create_dto = api.create_warm_pool.call_args.args[0]
+ assert create_dto.target == "us"
+
+ @pytest.mark.asyncio
+ async def test_update(self):
+ service, api = self._make_service()
+ api.update_warm_pool.return_value = _make_warm_pool_dto(pool=0)
+ result = await service.update("wp-123", pool=0)
+ assert isinstance(result, WarmPool)
+
+ @pytest.mark.asyncio
+ async def test_delete(self):
+ service, api = self._make_service()
+ await service.delete("wp-123")
+ api.delete_warm_pool.assert_called_once_with("wp-123")
diff --git a/sdk-ruby/lib/daytona/daytona.rb b/sdk-ruby/lib/daytona/daytona.rb
index 33b9eccad..e30865930 100644
--- a/sdk-ruby/lib/daytona/daytona.rb
+++ b/sdk-ruby/lib/daytona/daytona.rb
@@ -25,6 +25,9 @@ class Daytona
# @return [Daytona::SecretService]
attr_reader :secret
+ # @return [Daytona::WarmPoolService]
+ attr_reader :warm_pool
+
# @return [DaytonaApiClient::ObjectStorageApi]
attr_reader :object_storage_api
@@ -53,6 +56,7 @@ def initialize(config = Config.new)
@analytics_api_url_mutex = Mutex.new
@volume = VolumeService.new(DaytonaApiClient::VolumesApi.new(api_client), otel_state:)
@secret = SecretService.new(DaytonaApiClient::SecretApi.new(api_client), otel_state:)
+ @warm_pool = WarmPoolService.new(DaytonaApiClient::WarmPoolsApi.new(api_client), otel_state:)
@object_storage_api = DaytonaApiClient::ObjectStorageApi.new(api_client)
@snapshots_api = DaytonaApiClient::SnapshotsApi.new(api_client)
@snapshot = SnapshotService.new(
diff --git a/sdk-ruby/lib/daytona/sdk.rb b/sdk-ruby/lib/daytona/sdk.rb
index e84edd062..abe3ffbb6 100644
--- a/sdk-ruby/lib/daytona/sdk.rb
+++ b/sdk-ruby/lib/daytona/sdk.rb
@@ -47,6 +47,8 @@
require_relative 'util'
require_relative 'volume'
require_relative 'volume_service'
+require_relative 'warm_pool'
+require_relative 'warm_pool_service'
require_relative 'process'
module Daytona
diff --git a/sdk-ruby/lib/daytona/warm_pool.rb b/sdk-ruby/lib/daytona/warm_pool.rb
new file mode 100644
index 000000000..428693938
--- /dev/null
+++ b/sdk-ruby/lib/daytona/warm_pool.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Daytona
+ # A warm pool of ready-to-use Sandboxes for a snapshot. `current_size` versus
+ # `pool` is the pool's status: `current_size` is the number of ready warm
+ # sandboxes, `pool` is the desired number. `error_reason` is set when the
+ # pool cannot be filled.
+ class WarmPool
+ # @return [String]
+ attr_reader :id
+
+ # @return [String]
+ attr_reader :organization_id
+
+ # @return [String]
+ attr_reader :snapshot
+
+ # @return [String]
+ attr_reader :target
+
+ # @return [Integer]
+ attr_reader :pool
+
+ # @return [Integer]
+ attr_reader :current_size
+
+ # @return [Integer]
+ attr_reader :cpu
+
+ # @return [Integer]
+ attr_reader :mem
+
+ # @return [Integer]
+ attr_reader :disk
+
+ # @return [String]
+ attr_reader :os_user
+
+ # @return [Hash{String => String}]
+ attr_reader :env
+
+ # @return [String, nil]
+ attr_reader :error_reason
+
+ # @return [String]
+ attr_reader :created_at
+
+ # @return [String]
+ attr_reader :updated_at
+
+ # Initialize warm pool from DTO
+ #
+ # @param warm_pool_dto [DaytonaApiClient::WarmPool]
+ def initialize(warm_pool_dto)
+ @id = warm_pool_dto.id
+ @organization_id = warm_pool_dto.organization_id
+ @snapshot = warm_pool_dto.snapshot
+ @target = warm_pool_dto.target
+ # The generated client deserializes OpenAPI numbers as Float; these are all integral.
+ @pool = warm_pool_dto.pool.to_i
+ @current_size = warm_pool_dto.current_size.to_i
+ @cpu = warm_pool_dto.cpu.to_i
+ @mem = warm_pool_dto.mem.to_i
+ @disk = warm_pool_dto.disk.to_i
+ @os_user = warm_pool_dto.os_user
+ @env = warm_pool_dto.env
+ @error_reason = warm_pool_dto.error_reason
+ @created_at = warm_pool_dto.created_at
+ @updated_at = warm_pool_dto.updated_at
+ end
+ end
+end
diff --git a/sdk-ruby/lib/daytona/warm_pool_service.rb b/sdk-ruby/lib/daytona/warm_pool_service.rb
new file mode 100644
index 000000000..309c51684
--- /dev/null
+++ b/sdk-ruby/lib/daytona/warm_pool_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Daytona
+ class WarmPoolService
+ include Instrumentation
+
+ # Service for managing Daytona Warm Pools. Can be used to list, create, update and delete Warm Pools.
+ #
+ # @param warm_pools_api [DaytonaApiClient::WarmPoolsApi]
+ # @param otel_state [Daytona::OtelState, nil]
+ def initialize(warm_pools_api, otel_state: nil)
+ @warm_pools_api = warm_pools_api
+ @otel_state = otel_state
+ end
+
+ # Create a new Warm Pool.
+ #
+ # @param snapshot [String] The snapshot (ID or name) to keep warm sandboxes for
+ # @param pool [Integer] Number of warm sandboxes to keep ready
+ # @param target [String, nil] Target region; defaults to the organization default region
+ # @return [Daytona::WarmPool]
+ def create(snapshot, pool, target: nil)
+ WarmPool.new(warm_pools_api.create_warm_pool(DaytonaApiClient::CreateWarmPool.new(snapshot:, pool:, target:)))
+ end
+
+ # Delete a Warm Pool.
+ #
+ # @param warm_pool_id [String]
+ # @return [void]
+ def delete(warm_pool_id) = warm_pools_api.delete_warm_pool(warm_pool_id)
+
+ # List all Warm Pools.
+ #
+ # @return [Array]
+ def list
+ warm_pools_api.list_warm_pools.map { |warm_pool| WarmPool.new(warm_pool) }
+ end
+
+ # Update the desired size of a Warm Pool.
+ #
+ # @param warm_pool_id [String]
+ # @param pool [Integer] New desired number of warm sandboxes (0 drains the pool)
+ # @return [Daytona::WarmPool]
+ def update(warm_pool_id, pool)
+ WarmPool.new(warm_pools_api.update_warm_pool(warm_pool_id, DaytonaApiClient::UpdateWarmPool.new(pool:)))
+ end
+
+ instrument :create, :delete, :list, :update, component: 'WarmPoolService'
+
+ private
+
+ # @return [DaytonaApiClient::WarmPoolsApi]
+ attr_reader :warm_pools_api
+
+ # @return [Daytona::OtelState, nil]
+ attr_reader :otel_state
+ end
+end
diff --git a/sdk-ruby/spec/daytona/warm_pool_service_spec.rb b/sdk-ruby/spec/daytona/warm_pool_service_spec.rb
new file mode 100644
index 000000000..3fd515588
--- /dev/null
+++ b/sdk-ruby/spec/daytona/warm_pool_service_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+RSpec.describe Daytona::WarmPoolService do
+ let(:warm_pools_api) { instance_double(DaytonaApiClient::WarmPoolsApi) }
+ let(:service) { described_class.new(warm_pools_api) }
+
+ describe '#create' do
+ it 'creates a warm pool and returns a WarmPool model' do
+ dto = build_warm_pool_dto(snapshot: 'my-snapshot', pool: 5)
+ allow(warm_pools_api).to receive(:create_warm_pool).and_return(dto)
+
+ warm_pool = service.create('my-snapshot', 5)
+
+ expect(warm_pool).to be_a(Daytona::WarmPool)
+ expect(warm_pool.snapshot).to eq('my-snapshot')
+ expect(warm_pool.pool).to eq(5)
+ expect(warm_pools_api).to have_received(:create_warm_pool) do |request|
+ expect(request.snapshot).to eq('my-snapshot')
+ expect(request.pool).to eq(5)
+ expect(request.target).to be_nil
+ end
+ end
+
+ it 'passes the target region when given' do
+ dto = build_warm_pool_dto(target: 'eu')
+ allow(warm_pools_api).to receive(:create_warm_pool).and_return(dto)
+
+ service.create('my-snapshot', 5, target: 'eu')
+
+ expect(warm_pools_api).to have_received(:create_warm_pool) do |request|
+ expect(request.target).to eq('eu')
+ end
+ end
+ end
+
+ describe '#delete' do
+ it 'deletes a warm pool by id' do
+ allow(warm_pools_api).to receive(:delete_warm_pool).with('wp-123')
+
+ service.delete('wp-123')
+
+ expect(warm_pools_api).to have_received(:delete_warm_pool).with('wp-123')
+ end
+ end
+
+ describe '#list' do
+ it 'lists warm pools as WarmPool models' do
+ allow(warm_pools_api).to receive(:list_warm_pools).and_return([build_warm_pool_dto])
+
+ warm_pools = service.list
+
+ expect(warm_pools.length).to eq(1)
+ expect(warm_pools.first).to be_a(Daytona::WarmPool)
+ expect(warm_pools.first.current_size).to eq(3)
+ end
+ end
+
+ describe '#update' do
+ it 'updates the desired pool size' do
+ dto = build_warm_pool_dto(pool: 10)
+ allow(warm_pools_api).to receive(:update_warm_pool).and_return(dto)
+
+ warm_pool = service.update('wp-123', 10)
+
+ expect(warm_pool).to be_a(Daytona::WarmPool)
+ expect(warm_pool.pool).to eq(10)
+ expect(warm_pools_api).to have_received(:update_warm_pool) do |id, request|
+ expect(id).to eq('wp-123')
+ expect(request.pool).to eq(10)
+ end
+ end
+ end
+end
diff --git a/sdk-ruby/spec/spec_helper.rb b/sdk-ruby/spec/spec_helper.rb
index d2df17286..8efc7be3a 100644
--- a/sdk-ruby/spec/spec_helper.rb
+++ b/sdk-ruby/spec/spec_helper.rb
@@ -130,6 +130,27 @@ def build_secret_dto(overrides = {})
instance_double(DaytonaApiClient::Secret, **attrs)
end
+def build_warm_pool_dto(overrides = {})
+ attrs = {
+ id: 'wp-123',
+ organization_id: 'org-1',
+ snapshot: 'test-snapshot',
+ target: 'us',
+ pool: 5,
+ current_size: 3,
+ cpu: 2,
+ mem: 4,
+ disk: 10,
+ os_user: 'daytona',
+ env: {},
+ error_reason: nil,
+ created_at: '2025-01-01T00:00:00Z',
+ updated_at: '2025-01-01T00:00:00Z'
+ }.merge(overrides)
+
+ instance_double(DaytonaApiClient::WarmPool, **attrs)
+end
+
def build_snapshot_dto(overrides = {})
attrs = {
id: 'snap-123',
diff --git a/sdk-typescript/src/Daytona.ts b/sdk-typescript/src/Daytona.ts
index 3df5e1c16..838cfd0b6 100644
--- a/sdk-typescript/src/Daytona.ts
+++ b/sdk-typescript/src/Daytona.ts
@@ -13,6 +13,7 @@ import {
SandboxState,
SecretApi,
VolumesApi,
+ WarmPoolsApi,
ConfigApi,
} from '@daytona/api-client'
import type { GpuType, SandboxVolume } from '@daytona/api-client'
@@ -31,6 +32,7 @@ import type { ListSandboxesQuery } from './Sandbox'
import { SecretService } from './Secret'
import { SnapshotService } from './Snapshot'
import { VolumeService } from './Volume'
+import { WarmPoolService } from './WarmPool'
import { getPackageInfo, dynamicRequire } from './utils/Import'
import { EventDispatcher } from './utils/EventDispatcher'
import { EventSubscriptionManager } from './utils/EventSubscriptionManager'
@@ -227,6 +229,7 @@ export type CreateSandboxFromSnapshotParams = CreateSandboxBaseParams & {
* @property {VolumeService} volume - Service for managing Daytona Volumes
* @property {SnapshotService} snapshot - Service for managing Daytona Snapshots
* @property {SecretService} secret - Service for managing Daytona Secrets
+ * @property {WarmPoolService} warmPool - Service for managing Daytona Warm Pools
*
* @example
* // Using environment variables
@@ -283,6 +286,7 @@ export class Daytona implements AsyncDisposable {
public readonly volume: VolumeService
public readonly snapshot: SnapshotService
public readonly secret: SecretService
+ public readonly warmPool: WarmPoolService
/**
* Creates a new Daytona client instance.
@@ -372,6 +376,7 @@ export class Daytona implements AsyncDisposable {
this.configApi = new ConfigApi(configuration, '', axiosInstance)
this.volume = new VolumeService(new VolumesApi(configuration, '', axiosInstance))
this.secret = new SecretService(new SecretApi(configuration, '', axiosInstance))
+ this.warmPool = new WarmPoolService(new WarmPoolsApi(configuration, '', axiosInstance))
this.snapshot = new SnapshotService(
configuration,
new SnapshotsApi(configuration, '', axiosInstance),
diff --git a/sdk-typescript/src/WarmPool.ts b/sdk-typescript/src/WarmPool.ts
new file mode 100644
index 000000000..5edb0a649
--- /dev/null
+++ b/sdk-typescript/src/WarmPool.ts
@@ -0,0 +1,106 @@
+import { WarmPoolsApi } from '@daytona/api-client'
+import type { CreateWarmPool, UpdateWarmPool, WarmPool as WarmPoolDto } from '@daytona/api-client'
+import { WithInstrumentation } from './utils/otel.decorator'
+
+/**
+ * Represents a Daytona Warm Pool which keeps ready-to-use Sandboxes for a snapshot.
+ *
+ * `currentSize` versus `pool` is the pool's status: `currentSize` is the number of ready
+ * warm sandboxes, `pool` is the desired number. `errorReason` is set when the pool cannot
+ * be filled.
+ *
+ * @property {string} id - Unique identifier for the Warm Pool
+ * @property {string} organizationId - Organization ID that owns the Warm Pool
+ * @property {string} snapshot - Snapshot the pool keeps warm sandboxes for
+ * @property {string} target - Target region of the pool
+ * @property {number} pool - Desired number of warm sandboxes
+ * @property {number} currentSize - Current number of ready warm sandboxes in the pool
+ * @property {number} cpu - CPU cores per sandbox
+ * @property {number} mem - Memory per sandbox in GiB
+ * @property {number} disk - Disk per sandbox in GiB
+ * @property {string} osUser - OS user of the warm sandboxes
+ * @property {Record} env - Environment variables of the warm sandboxes
+ * @property {string | null} [errorReason] - Reason the pool cannot be filled, if any
+ * @property {string} createdAt - Date and time when the Warm Pool was created
+ * @property {string} updatedAt - Date and time when the Warm Pool was last updated
+ */
+export type WarmPool = WarmPoolDto & { __brand: 'WarmPool' }
+
+/**
+ * Service for managing Daytona Warm Pools.
+ *
+ * This service provides methods to list, create, update, and delete Warm Pools.
+ *
+ * @class
+ */
+export class WarmPoolService {
+ constructor(private warmPoolsApi: WarmPoolsApi) {}
+
+ /**
+ * Lists all Warm Pools in the organization.
+ *
+ * @returns {Promise} List of all Warm Pools
+ *
+ * @example
+ * const daytona = new Daytona();
+ * const pools = await daytona.warmPool.list();
+ * pools.forEach(pool => console.log(`${pool.snapshot}: ${pool.currentSize}/${pool.pool} ready`));
+ */
+ @WithInstrumentation()
+ async list(): Promise {
+ const response = await this.warmPoolsApi.listWarmPools()
+ return response.data as WarmPool[]
+ }
+
+ /**
+ * Creates a new Warm Pool.
+ *
+ * @param {CreateWarmPool} params - Parameters for the new Warm Pool
+ * @returns {Promise} The newly created Warm Pool
+ * @throws {DaytonaConflictError} If a Warm Pool for the same snapshot and region already exists
+ *
+ * @example
+ * const daytona = new Daytona();
+ * const pool = await daytona.warmPool.create({ snapshot: 'my-snapshot', pool: 5 });
+ * console.log(`Created warm pool ${pool.id} in ${pool.target}`);
+ */
+ @WithInstrumentation()
+ async create(params: CreateWarmPool): Promise {
+ const response = await this.warmPoolsApi.createWarmPool(params)
+ return response.data as WarmPool
+ }
+
+ /**
+ * Updates the desired size of a Warm Pool.
+ *
+ * @param {string} warmPoolId - ID of the Warm Pool to update
+ * @param {UpdateWarmPool} params - Fields to update (`pool: 0` drains the pool)
+ * @returns {Promise} The updated Warm Pool
+ * @throws {DaytonaNotFoundError} If the Warm Pool does not exist
+ *
+ * @example
+ * const daytona = new Daytona();
+ * const pool = await daytona.warmPool.update("warm-pool-id", { pool: 10 });
+ */
+ @WithInstrumentation()
+ async update(warmPoolId: string, params: UpdateWarmPool): Promise {
+ const response = await this.warmPoolsApi.updateWarmPool(warmPoolId, params)
+ return response.data as WarmPool
+ }
+
+ /**
+ * Deletes a Warm Pool.
+ *
+ * @param {string} warmPoolId - ID of the Warm Pool to delete
+ * @returns {Promise}
+ * @throws {DaytonaNotFoundError} If the Warm Pool does not exist
+ *
+ * @example
+ * const daytona = new Daytona();
+ * await daytona.warmPool.delete("warm-pool-id");
+ */
+ @WithInstrumentation()
+ async delete(warmPoolId: string): Promise {
+ await this.warmPoolsApi.deleteWarmPool(warmPoolId)
+ }
+}
diff --git a/sdk-typescript/src/__tests__/Daytona.test.ts b/sdk-typescript/src/__tests__/Daytona.test.ts
index 1ac3da63f..daac4774f 100644
--- a/sdk-typescript/src/__tests__/Daytona.test.ts
+++ b/sdk-typescript/src/__tests__/Daytona.test.ts
@@ -52,6 +52,7 @@ jest.mock(
ConfigApi: jest.fn(() => mockConfigApi),
VolumesApi: jest.fn(() => mockVolumesApi),
SecretApi: jest.fn(() => mockSecretApi),
+ WarmPoolsApi: jest.fn(() => ({})),
SandboxState: {
PENDING_BUILD: 'pending_build',
STARTED: 'started',
diff --git a/sdk-typescript/src/__tests__/WarmPool.test.ts b/sdk-typescript/src/__tests__/WarmPool.test.ts
new file mode 100644
index 000000000..83dc86371
--- /dev/null
+++ b/sdk-typescript/src/__tests__/WarmPool.test.ts
@@ -0,0 +1,54 @@
+import { createApiResponse } from './helpers'
+import { WarmPoolService } from '../WarmPool'
+
+jest.mock('@daytona/api-client', () => ({}), { virtual: true })
+
+describe('WarmPoolService', () => {
+ const warmPoolsApi = {
+ listWarmPools: jest.fn(),
+ createWarmPool: jest.fn(),
+ updateWarmPool: jest.fn(),
+ deleteWarmPool: jest.fn(),
+ }
+ const service = new WarmPoolService(warmPoolsApi as unknown as never)
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('lists warm pools', async () => {
+ warmPoolsApi.listWarmPools.mockResolvedValue(createApiResponse([{ id: 'wp1', snapshot: 'snap', pool: 3 }]))
+
+ await expect(service.list()).resolves.toEqual([{ id: 'wp1', snapshot: 'snap', pool: 3 }])
+ })
+
+ it('creates a warm pool with the given params', async () => {
+ warmPoolsApi.createWarmPool.mockResolvedValue(createApiResponse({ id: 'wp2', snapshot: 'snap', pool: 5 }))
+
+ await expect(service.create({ snapshot: 'snap', pool: 5 })).resolves.toEqual({
+ id: 'wp2',
+ snapshot: 'snap',
+ pool: 5,
+ })
+ expect(warmPoolsApi.createWarmPool).toHaveBeenCalledWith({ snapshot: 'snap', pool: 5 })
+ })
+
+ it('updates the pool size', async () => {
+ warmPoolsApi.updateWarmPool.mockResolvedValue(createApiResponse({ id: 'wp3', pool: 10 }))
+
+ await expect(service.update('wp3', { pool: 10 })).resolves.toEqual({ id: 'wp3', pool: 10 })
+ expect(warmPoolsApi.updateWarmPool).toHaveBeenCalledWith('wp3', { pool: 10 })
+ })
+
+ it('deletes a warm pool', async () => {
+ await service.delete('wp4')
+ expect(warmPoolsApi.deleteWarmPool).toHaveBeenCalledWith('wp4')
+ })
+
+ it('propagates create failures', async () => {
+ const error = new Error('conflict')
+ warmPoolsApi.createWarmPool.mockRejectedValue(error)
+
+ await expect(service.create({ snapshot: 'snap', pool: 1 })).rejects.toBe(error)
+ })
+})
diff --git a/sdk-typescript/src/index.ts b/sdk-typescript/src/index.ts
index 832f7e742..7fc1762a9 100644
--- a/sdk-typescript/src/index.ts
+++ b/sdk-typescript/src/index.ts
@@ -80,6 +80,7 @@ export { Sandbox } from './Sandbox'
export type { ListSandboxesQuery, SandboxMetrics } from './Sandbox'
export type { Secret, CreateSecretParams, UpdateSecretParams, ListSecretsQuery, ListSecretsResponse } from './Secret'
export type { CreateSnapshotParams } from './Snapshot'
+export type { WarmPool } from './WarmPool'
export { ComputerUse, Mouse, Keyboard, Screenshot, Display, Accessibility } from './ComputerUse'
export type {
BarChart,
@@ -109,6 +110,7 @@ export {
ListSecretsPaginatedSortEnum,
ListSecretsPaginatedOrderEnum,
} from '@daytona/api-client'
+export type { CreateWarmPool, UpdateWarmPool } from '@daytona/api-client'
export type {
FileInfo,
GitStatus,