Java SDK
One Java client for every Runtime Cloud product.
com.withruntime:withruntime needs Java 17 or later and nothing beyond the
JDK: no HTTP library, no JSON library. It is a named module,
com.withruntime.
xml<dependency> <groupId>com.withruntime</groupId> <artifactId>withruntime</artifactId> <version>0.31.1</version></dependency>With Gradle: implementation("com.withruntime:withruntime:0.31.1").
The client finds its key by itself. It uses RUNTIME_API_KEY when that is
set, and otherwise the connection this machine saved (any npx withruntime
command connects it, with one browser approval).
On a server, put a key from https://withruntime.com/account/keys in
RUNTIME_API_KEY, stored in your secret manager. Never put it in source code, a
URL or a command-line argument. With no key anywhere, RuntimeClient.create()
throws RuntimeCloudException.Authentication whose code is missing_api_key
and whose hint says how to get one.
Hello, sandbox#
javaimport com.withruntime.*;public class Hello { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox())) { sbx.files().write("/workspace/invoice.py", "print(sum([125, 250, 375]))\n"); CommandResult result = sbx.exec("python3 /workspace/invoice.py", new ExecOptions().check(true)); System.out.print(result.stdout()); } }}create returns once the sandbox is running. Closing the sandbox deletes it.
new CreateSandbox().keep() makes closing stop the sandbox instead, and keeps
its disk. delete() deletes a sandbox at any time. With no options, you get the
free usage while it lasts, the default region, and 2 vCPU, 4 GiB of memory and a
4 GiB disk. The sandbox runs while it works and pauses itself when idle. It has
no time limit unless you set one with timeoutSeconds(seconds) (60 to 86,400).
The free usage is spent first, then prepaid credit. funding(...) is accepted
and ignored. Every field is optional:
javaimport com.withruntime.*;public class Sized { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); Sandbox sbx = runtime .sandboxes() .create( new CreateSandbox() .name("tests-42") .label("team", "search") .vcpu(2) .memoryMiB(4096) .diskMiB(8192) .timeoutSeconds(900) .onLeaseEnd("stop") .network(new NetworkRules().internet(true).allow("repo.maven.apache.org"))); System.out.println(sbx.id() + " " + sbx.info().funding() + " " + sbx.info().endsAt()); sbx.stop(); }}A field newer than the SDK goes in with .set("field", value).
Run commands#
exec runs a string under bash -c. execArgv runs a program directly, with
no shell. Use it for untrusted arguments.
javaimport com.withruntime.*;import java.time.Duration;import java.util.List;public class Commands { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { sbx.exec("mkdir -p app && echo 'print(1 + 1)' > app/main.py"); CommandResult run = sbx.execArgv( List.of("python3", "main.py"), new ExecOptions() .cwd("/workspace/app") .env("API_TOKEN", System.getenv("API_TOKEN")) .timeout(Duration.ofMinutes(2))); if (run.exitCode() != 0) System.err.print(run.stderr()); System.out.print(run.stdout()); sbx.exec("for i in 1 2 3; do echo line $i; sleep 1; done", new ExecOptions().onStdout(System.out::print)); try (EventStream<OutputEvent> events = sbx.execStream("node --version", new ExecOptions())) { for (OutputEvent event : events) if (event.type().equals("stdout")) System.out.print(event.data()); } } }}envpasses secrets to a command. Runtime never echoes them back. Never put a secret in the command line.stdingives the command input, then closes it..user("root")runs the command as root, with nosudoin front, keeping itsenvandPATH;HOMEis/root.SpawnOptionstakes it too, andProcessInfo.user()says which user a process runs as.- The default timeout is 60 seconds; the maximum is 24 hours. A timeout is a
result (
timedOut()is true, with the output so far), not an exception. check(true)throwsRuntimeCloudException.Command, with the exit code and output, on a non-zero exit or a timeout.- A result holds all of the output, up to 256 MiB of each stream (no limit
with
onStdoutoronStderr).stdoutBytes()andstderrBytes()give the bytes each stream wrote.maxOutputBytes(n)keeps at mostnbytes of each stream;truncated()is thentrue. WithonStdout,onStderr, or a timeout over a minute, the command streams. When the server ends a long stream, it resumes by itself. No output is dropped.
Background processes#
javaimport com.withruntime.*;public class Processes { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { SandboxProcess server = sbx.spawn("python3 -m http.server 8000", new SpawnOptions().cwd("/workspace")); SandboxProcess repl = sbx.spawn("python3 -i -q", new SpawnOptions().pipeStdin()); repl.write("print(21 * 2)\n", true); System.out.print(repl.waitFor().stdout()); for (ProcessInfo process : sbx.processes()) System.out.println(process.id() + " " + process.state()); server.kill("SIGTERM"); } }}A process outlives your connection. Get it back with sbx.process(id).
write tracks input offsets, so a retried write is never typed twice.
Files#
javaimport com.withruntime.*;import java.nio.file.Path;public class FilesExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create()) { sbx.files().write("/workspace/data/input.csv", "a,b\n1,2\n"); System.out.print(sbx.files().readText("/workspace/data/input.csv")); for (FileEntry entry : sbx.files().list("/workspace", 2, null)) System.out.println(entry.type() + " " + entry.size() + " " + entry.path()); sbx.files().mkdir("/workspace/out", true); sbx.files().rename("/workspace/data/input.csv", "/workspace/out/input.csv", false); sbx.files().upload(Path.of("src"), "/workspace/project/src"); sbx.files().download("/workspace/out", Path.of("out")); } }}write creates parent directories and replaces the file atomically. A file over
1 MiB goes in parallel chunks, each checked by SHA-256. stat returns null for
a path that does not exist. upload and download move a whole directory as
one gzipped tar archive, through the API's folder routes. The sandbox packs and
unpacks it with tar (files in the API guide). A large folder
uploads in parts. A downloaded folder is unpacked as it arrives. It lands in place
only after all of it has arrived. A download cut short fails with
download_incomplete and writes nothing.
Pause, wake, fork and snapshot#
javaimport com.withruntime.*;import java.time.Duration;import java.util.List;public class Lifecycle { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox())) { sbx.pause(); // memory and files are kept; compute billing stops Sandbox again = runtime.sandboxes().get(sbx.id()); again.wake(Duration.ofMinutes(20)); again.extend(Duration.ofMinutes(10)); List<Sandbox> copies = again.fork(new ForkOptions().count(2)); for (Sandbox copy : copies) copy.stop(); Snapshot snapshot = again.snapshot(new SnapshotOptions().name("ready").retentionDays(7)); try (Sandbox fromSnapshot = runtime.sandboxes().create(new CreateSandbox().snapshot(snapshot.id()))) { System.out.println(fromSnapshot.id()); } runtime.snapshots().delete(snapshot.id()); } }}A fork copies a sandbox as it is now, with its files, memory and running
processes. It runs on its server while that server has room, and on another
server when it is full. See JavaScript for what forks and
snapshots keep and how they are billed. If a copy fails, the exception's
details().get("startedSandboxIds") names the copies that did start.
wake(Duration) and extend(Duration) set and move a time limit, for a
sandbox that has one. On a sandbox without a time limit, extend changes
nothing. sbx.keepAlive(every, margin, onError) moves the time limit on from
your process until stop(). runtime.sandboxes().getOrCreate(name, options)
returns the sandbox with that name, woken or restarted, or creates it.
Images, volumes, network rules and secrets#
javaimport com.withruntime.*;import java.util.List;import java.util.Map;public class Storage { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); Image image = runtime .images() .build( new CreateImage().name("data").recipe(Map.of("pip", List.of("pandas"), "apt", List.of("jq"))), line -> System.out.println(line.getString("text"))); Volume volume = runtime.volumes().create(10240, "cache", null); try (Sandbox sbx = runtime.sandboxes().create(new CreateSandbox().image(image.id()).volume(volume.id(), "/data", null))) { System.out.print(sbx.exec("python3 -c 'import pandas; print(pandas.__version__)'").stdout()); JsonObject cell = sbx.interpreter().run("import math\nround(math.pi * 2, 3)"); System.out.println(cell.getObjects("results").get(0).getObject("data").getString("text/plain")); sbx.network().set(new NetworkRules().internet(true).allow("pypi.org", "*.pythonhosted.org")); } JsonObject secret = runtime.secrets().set("OPENAI_API_KEY", "sk-...", List.of("api.openai.com")); System.out.println(secret.getString("placeholder")); // what sandboxes see in $OPENAI_API_KEY }}images().build waits until the image is ready. It throws with code
image_failed when the build fails. images().create queues the build and
returns. A Dockerfile build takes dockerfile(text) and contextDir(path), the
folder docker build would read, with its .dockerignore. Only the parts the
server does not have yet are uploaded. See custom images. A volume
lives on one server. Its backups can restore a new volume onto another host.
sbx.desktop() drives a Linux desktop in the sandbox.
Metrics, events and webhooks#
javaimport com.withruntime.*;import java.nio.charset.StandardCharsets;import java.util.List;public class Hooks { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); JsonObject hook = runtime.webhooks().create("https://example.com/hooks/runtime", null, null); String secret = hook.getString("secret"); // shown once; keep it // In your endpoint, with the raw body and the Runtime-Signature header: byte[] body = "{}".getBytes(StandardCharsets.UTF_8); String header = "t=0,v1=0"; try { JsonObject event = Products.Webhooks.verify(body, header, List.of(secret)); System.out.println(event.getString("type")); } catch (SecurityException forged) { System.out.println("refused: " + forged.getMessage()); } for (JsonObject event : runtime.events().list(null, "sandbox.stopped")) System.out.println(event.getString("createdAt") + " " + event.getString("type")); }}sbx.metrics("15m") returns the sandbox's measured CPU and memory, and
runtime.otel().create(...) pushes events and metrics to an OpenTelemetry
endpoint. See metrics and webhooks.
Errors and retries#
javaimport com.withruntime.*;public class Errors { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); try { runtime.sandboxes().get("00000000-0000-4000-8000-000000000000"); } catch (RuntimeCloudException.NotFound missing) { System.out.println("no such sandbox"); } catch (RuntimeCloudException failure) { System.out.println(failure.code() + " " + failure.hint() + " " + failure.requestId()); } }}Every exception is a RuntimeCloudException (unchecked) with code(),
getMessage(), hint(), requestId(), status() and details(). Its
subclasses are Authentication, PermissionDenied, NotFound, Conflict,
InvalidRequest, RateLimited, ServiceUnavailable, Connection and
Command. They are the same classes as in the JavaScript, Python and Go SDKs.
no_capacity is ServiceUnavailable. no_credit_running_limit, and a
quota_exceeded on what runs at the same time, are RateLimited. A
quota_exceeded on what is kept is Conflict.
Every write carries an idempotency key. The client makes it for you and keeps it
across its own retries. Transport failures, and every response that says
retryable: true, are retried after its Retry-After. Retries stop at the
maximum number of retries. Each retry uses the same key, so a retry never makes
two sandboxes or runs a command twice. A wait over 60 seconds is not slept on.
The error returns immediately, with retryAfterMs. A response that ran nothing
(guest_busy, busy, rate_limited, sandbox_starting) is sent again until
the call's deadline.
Without a deadline, it is sent again for five minutes, past the maximum.
retryable() says whether trying the same call again may work.
A create, wake or restart waits for room. When all four slots
without credit are taken (no_credit_running_limit), the account is at its limit
(quota_exceeded), or the region is full (no_capacity), sandboxes().create
waits and sends the same request again, for up to two minutes. Set
waitForCapacity on the client builder, or on one CreateSandbox, wake or
restart. Zero fails immediately. A sandbox stays on its server, so a wake or
restart refused with no_capacity is sent again with the same idempotency key
until room frees. A fork waits the same way when no server has room for a copy;
set waitForCapacity on its ForkOptions to change the wait.
An owner, admin or developer can make a read-only key and set a daily spending
limit on a key at API keys.
runtime.limits().get() reads both. Past the limit, a create, wake or extension
throws with code spending_limit_reached, and it is not retried.
Configuration#
javaimport com.withruntime.*;import java.time.Duration;public class Configured { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.builder() .apiKey(System.getenv("MY_RUNTIME_KEY")) .timeout(Duration.ofMinutes(2)) .maxRetries(4) .waitForCapacity(Duration.ofMinutes(10)) .build(); System.out.println(runtime.me().orgId()); System.out.println(runtime.request("GET", "/v1/volumes", null, null)); // any endpoint, same rules }}RUNTIME_API_URLpoints the client at another API origin, asbaseUrldoes.- Code inside a Runtime sandbox calls Runtime's API at
http://runtime.internal(Runtime's API from inside a sandbox). - The client is safe for concurrent use. Make one and share it. It keeps its
connections open and holds at most 32 calls in flight (
maxConnections). - It honours
HTTPS_PROXYandNO_PROXY.httpClientsupplies your ownjava.net.http.HttpClient. - Money is integer microdollars: 1,000,000 is one US dollar.
runtime.usage()returns the account's balance exactly, asBigInteger. runtime.usageExport(since, until, cursor, limit)reads one page of settled usage over a UTC range, with its server CSV. FollownextCursor()until it is null.finalThrough()is the time before which no more charges will settle. A row'sisFinal()is true once its resource is deleted and can be charged nothing more (export settled usage).
Network products and backups#
runtime.domains(), ports(), addresses() and tunnel() manage account
networking. Domains and ports name the sandbox they serve. tunnel() is the
account's WireGuard network. It is separate from a sandbox's TCP forwarding.
Responses keep the API's complete fields through JsonObject. Use getLong
for integer money, and getInstant for funding deadlines. When credit runs out,
an address or tunnel reservation stays until it is released. Traffic stops during
the unfunded time.
javaimport com.withruntime.*;import java.util.List;public class NetworkProductsExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); JsonObject domain = runtime.domains().add("app.example.org", args[0], 8080); System.out.println(domain.getObjects("records")); // Publish these DNS records, then verify. JsonObject address = runtime.ips().reserve(4); System.out.println(address.getString("address")); System.out.println(address.getLong("rateMicros")); runtime.networks().wireguard().create("10.66.0.0/24"); JsonObject peer = runtime.networks().wireguard().addPeer("laptop", null, List.of()); // Save peer.getString("config") privately, even when configReady is false. // Its private key is shown once; generated-key calls are not automatically retried. System.out.println(peer.getBoolean("configReady")); System.out.println(runtime.sso().get()); // Owner console manages SSO settings. }}volumes().backup, backups, getBackup, deleteBackup, setBackupPolicy
and restore manage backups. setBackupSchedule(id, 60, null, Map.of("hourly", 24, "daily", 7)) backs the volume up every 60 minutes (or pass a UTC cron
instead of the minutes) and keeps backups by those counts.
clearBackupSchedule(id, null) goes back to daily. A restore creates a new
volume. It does not replace the original. A backup's backedUp field is true
once its copy has been checked off the source host. Writes after that backup are
not in it.
javaimport com.withruntime.*;public class VolumeBackupExample { public static void main(String[] args) { RuntimeClient runtime = RuntimeClient.create(); runtime.volumes().setBackupPolicy(args[0], true, 7); JsonObject backup = runtime.volumes().backup(args[0], "before-update"); if ("ready".equals(backup.getString("state"))) { Volume restored = runtime.volumes().restore(backup.getString("id"), "restored"); System.out.println(restored.id()); } }}File permissions and watching#
Files.WriteOptions.mode(0755) sets permissions atomically on current guest
images. 0 means no permissions, rather than the default. Older images use
the exec permission for a chmod fallback. A supplied write idempotency key
covers the whole upload, including completed retries. files().chmod changes
an existing file's permissions.
A watch's events() follows its cursor across server handovers. Loss notices
(overflow or lost) mean the caller should rescan. Closing the event stream
stops reading. watch.stop() also stops the guest watch. A later events()
resumes after a pause or an earlier reader close. Only one reader uses a watch
at a time. files().watches() also lists watches, polls and stops them by ID.
javaimport com.withruntime.*;import java.util.Map;public class FileWatchExample { public static void main(String[] args) { Sandbox sandbox = RuntimeClient.create().sandboxes().get(args[0]); sandbox.files().write("/workspace/run.sh", "#!/bin/sh\necho ready\n", new Files.WriteOptions().mode(0755).idempotencyKey("install-run-script-v1")); FileWatch watch = sandbox.files().watch("/workspace", Map.of("recursive", true)); try (EventStream<JsonObject> events = watch.events()) { for (JsonObject event : events) { System.out.println(event); if ("paused".equals(event.getString("k"))) break; } } finally { watch.stop(); } }}Terminals and TCP forwarding#
A terminal carries binary input/output over an authenticated WebSocket. Read output while writing input. Close it to release a blocked reader or writer. Interrupting its reader also closes it. Its opening deadline follows the client's timeout. Closing an attached terminal does not stop its existing process. A new terminal's shell ends with the connection.
openTunnel() multiplexes loopback TCP streams. A stream's closeWrite()
sends input EOF and keeps reading the response. Each stream holds at most
1 MiB of unread response data. If a stream exceeds that bound, only that stream
fails with tunnel_receive_overflow, after delivering its buffered bytes.
portForward() copies both directions for each local connection. It closes
its listener and active connections together.
javaimport com.withruntime.*;import java.net.InetSocketAddress;import java.util.Map;public class TerminalForwardExample { public static void main(String[] args) throws Exception { Sandbox sandbox = RuntimeClient.create().sandboxes().get(args[0]); try (Terminal terminal = sandbox.terminal(Map.of("command", "printf ready"))) { terminal.input().transferTo(System.out); System.out.println(terminal.exitCode()); } try (PortForward forward = sandbox.portForward(5432, new InetSocketAddress("127.0.0.1", 0))) { System.out.println(forward.address()); // Connect your local database client here. System.in.read(); } }}Bucket mounts and recordings#
sbx.mounts().add(options) takes provider, bucket, path and the name of a
Runtime secret, keeping the bucket key outside the sandbox. list() and
remove(path) manage its lifecycle.
sbx.desktop().recordings() has start, get, list, stop, download
and delete. Downloads return MP4 bytes. Recordings live on the sandbox's
own disk and count toward that disk's space.