This post details bytebox, a dependency and runtime to get Java to run on Serverless Architecture via Cloudflare Workers. It uses TeaVM to compile Java into WebAssembly usable by the Cloudflare Workers runtime.
It comes in multiple forms: you can use it as a Gradle plugin to output a deployable worker or a WebAssembly module, and comes as a NPM module to assemble workers by hand. Java dependencies are available via Maven Central and the Gradle Plugin Portal.
An important note about Cloudflare Workers vs Java is that Cloudflare Workers is a single-threaded synchronous state, so all operations are blocking and concurrency is not possible from Java. java.util.concurrent and other threading operations are not included and will result in a compilation error from trying to use them. The GitHub repository lists the full scope of other platform limitations and out-of-scope items, including the use of Process and ProcessBuilder, dynamic class loading, and inbound TCP listening or UDP handlers with DatagramSocket or ServerSocket.
Currently supported are Java 21 and 25 LTS versions. Documentation for both the Java API and the TypeScript API is available at bytebox.gmitch215.dev.
Quick Start
Copied from the README page:
package com.example;
import dev.gmitch215.bytebox.*;
public class HelloWorker implements Worker {
@Override
public Response fetch(Request request, Env env, ExecutionCtx ctx) {
return Bytebox.response("hello from Java");
}
}
In your Gradle configuration:
plugins {
java
id("dev.gmitch215.bytebox") version "1.0.0"
}
repositories {
mavenCentral()
}
dependencies {
implementation("dev.gmitch215:bytebox-core:1.0.0")
}
// ...
bytebox {
handlerClass = "com.example.HelloWorker" // to handler class
wrangler {
name = "hello-world" // name of cfw
compatibilityDate = "2026-08-22" // compatibility date of cfw
}
}
The Gradle plugin will compile your code to WebAssembly using TeaVM and output the necessary worker scaffholding to use wrangler dev or wrangler deploy:
./gradlew buildWorker # build wrangler.jsonc, webassembly module, entrypoint
./gradlew workerDeploy # run wrangler deploy to logged-in account
Bindings
All bindings available on Cloudflare Workers are supported in bytebox. The Gradle plugin allows you to easily add one or multiple bindings for your generated wrangler.jsonc, and provides default names if you so choose.
bytebox {
bindings {
kv() // KV
kv("SESSIONS") { id = "abc123" } // an explicit name and a remote id
d1() // DB
r2() // BLOB
durableObject("Counter") // DO_COUNTER
}
}
You can also declare them like so:
bytebox {
bindings(KV, D1, D1, KV) // KV, DB, DB_2, KV_2
}
A Gradle task named bindingsReport is available to get detailed information about the bindings you have declared in your project.
JSON and Serialization
Many Cloudflare Workers handle and respond with JSON outputs. bytebox comes with buult-in utilities to parse and handle JSON.
To automatically serialize a class or a record, annotate it with @JsonType:
@JsonType
public record Order(String sku, int quantity, long total, List<String> tags) { // ... }
Bytebox.json serializes it to JSON:
Order order = request.json(Order.class);
return Bytebox.json(order, Order.class);
java.io is also supported normally, and requires no additional code.
byte[] wire = Serial.encode(order);
Order back = Serial.decode(wire, Order.class);
Dependencies & Runtime
Java Standard Libraries
bytebox and TeaVM provide polyfills and runtime capabilities for you to use the majority of the Java Standard Library without any code modification. The Gradle plugin will automatically rewrite any class files or dependencies routed to a systems class that requires a polyfill, so no code modification is required.
ZonedDateTime local = Instant.now().atZone(ZoneId.of("America/New_York"));
HttpResponse<String> answer = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(URI.create("https://example.com")).build(), ofString());
Libraries like java.time, java.net, java.io, java.util.regex, and many others can be written like you would normally.
package com.example;
import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Ordinary Java, on a platform that has none of it.
*
* <p>Nothing here is a bytebox API. It is {@code java.time}, {@code java.net.http},
* {@code java.util.regex} and {@code String.format}, written the way they are written anywhere, and
* the compiler points each reference at an implementation that works on this runtime. That is what
* makes an unmodified library compile: the library does not know it is being retargeted.
*
* <p>What each one costs, and where each one differs from a JVM, is in the technical report. The two
* differences worth knowing at the call site are here as comments.
*/
public class StandardLibraryWorker implements Worker {
private static final Pattern SINCE = Pattern.compile("since=(?<date>\\d{4}-\\d{2}-\\d{2})");
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
@Override
public Response fetch(Request request, Env env, ExecutionCtx ctx) {
LocalDate since = LocalDate.of(2026, 1, 1);
Matcher asked = SINCE.matcher(request.getUrl());
if (asked.find()) since = LocalDate.parse(asked.group("date"));
// the clock is pinned between I/O, so this is the time the invocation began and does not move
Instant now = Instant.now();
ZonedDateTime local = now.atZone(ZoneId.of("America/New_York"));
long days = ChronoUnit.DAYS.between(since, local.toLocalDate());
StringBuilder body = new StringBuilder();
body.append(local.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)).append('\n');
// %,d groups every three digits here, which is right for the locales that group in threes
body.append(String.format("%,d days since %s%n", days, since));
body.append(String.format("%-12s %8.2f%%%n", "elapsed", (days * 100.0) / 365));
body.append(upstream()).append('\n');
return Bytebox.response(body.toString());
}
/** A request through the modern client, which is {@code fetch} underneath and suspends the fiber. */
private String upstream() {
try {
HttpResponse<String> answer = CLIENT.send(
HttpRequest.newBuilder(URI.create("https://example.com/"))
.header("Accept", "text/html")
.timeout(Duration.ofSeconds(3))
.build(),
HttpResponse.BodyHandlers.ofString()
);
return String.format(
"upstream %d, %,d bytes",
answer.statusCode(),
answer.body().length()
);
} catch (IOException | InterruptedException failed) {
return "upstream unreachable: " + failed.getMessage();
}
}
}
npm Dependencies
The bytebox Gradle plugin supports adding NPM dependencies to your worker, and comes with static analysis to generate supported types based on the npm module to use in your Java code.
bytebox {
npm("nanoid", "^5.0.9")
npmBindings("nanoid")
}
npm installs the dependency, and npmBindings tells the plugin to read the *.d.ts declarations in the npm module to generate Java source files that you can use to work with the package.
You can also use the @JSBody annotation that TeaVM provides with the native keyword to wire it directly:
@JSBody(
params = "size",
imports = @JSBodyImport(alias = "nanoid", fromModule = "nanoid"),
script = "return nanoid.nanoid(size);"
)
private static native String id(int size);
Third-Party Java Dependencies
Dependencies can be declared as normal and they will be compiled in with the worker.
dependencies {
implementation("com.example:foo:1.0.0")
}
Cloudflare Workers' free plan has a hard limit of 3 MiB after gzip on the bundle, with paid increasing to 10 MiB after gzip, so installing heavy Java dependencies should be done with care. TeaVM only includes parts of the Java runtime that it detects is being used, and does not include the full runtime on compilation (unless everything is being used). The estimation for each runtime feature's cost is provided below.
| Feature | Added, gzipped |
|---|---|
| streams, collections, reflection | 3.0 to 3.8 KB each |
| threads | 6.8 KB |
java.net.Socket |
9.8 KB |
BigDecimal |
12.7 KB |
java.util.regex |
13.3 KB |
java.net.URL and HttpURLConnection
|
13.3 KB |
java.time with zones |
23.8 KB |
String.format |
33.0 KB |
java.net.http |
54.1 KB |
Cloudflare Runtime Libraries
Cloudflare Workers provides runtime libraries like cloudflare:sockets, cloudflare:mail, and many other built-ins available to you. bytebox provides Java API around these out of the box so you can use them how you please.
Examples
Borrowed from the GitHub repository's snippets.
Cron Job
All triggers are supported in bytebox, and cron is not an exception.
package com.example;
import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Cron;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Scheduled;
import dev.gmitch215.bytebox.builtin.Clock;
/**
* A Worker with no HTTP handler at all.
*
* <p>Implementing only {@code Scheduled} means the generated Worker exports only {@code scheduled},
* and the generated configuration carries only the trigger. Nothing is exported for a trigger the
* handler does not implement.
*
* <p>An account gets 5 Cron Triggers on the free plan and 250 on paid, counted across every Worker
* rather than per Worker. A scheduled invocation gets 15 minutes rather than a request's allowance,
* and a throw is logged without a retry.
*/
public class NightlyWorker implements Scheduled {
@Override
public void scheduled(Cron cron, Env env, ExecutionCtx ctx) {
Bytebox.log("fired for " + cron.expression() + ", due at " + Clock.iso(cron.scheduledAt()));
env.kv().put("last-run", Clock.isoNow());
}
}
Durable Objects
Durable Objects are supported out-of-the-box with bytebox. They can either be used directly or be implemented over as a class.
package com.example;
import dev.gmitch215.bytebox.Bytebox;
import dev.gmitch215.bytebox.Env;
import dev.gmitch215.bytebox.ExecutionCtx;
import dev.gmitch215.bytebox.Request;
import dev.gmitch215.bytebox.Response;
import dev.gmitch215.bytebox.Worker;
import dev.gmitch215.bytebox.binding.DurableObjectNamespace;
import dev.gmitch215.bytebox.js.TSObject;
/**
* Routes to a Durable Object, which is where state that has to be exact belongs.
*
* <p>One instance per id, in one place, so two requests for the same id reach the same instance and
* see each other's writes. That is the difference from the kv-counter sample, where two regions can
* both read the same value and both write the next one.
*
* <p>The Durable Object class itself is JavaScript: it extends Cloudflare's own base class, which is
* a JavaScript class the runtime instantiates. What Java owns is the routing and the calls.
*/
public class CounterWorker implements Worker {
@Override
public Response fetch(Request request, Env env, ExecutionCtx ctx) {
DurableObjectNamespace counters = env.durableObject("DO_COUNTER");
// the id is derived from the path, so every path gets its own instance
var counter = counters.byName(request.path());
TSObject count = counter.rpc("increment");
return Bytebox.response(request.path() + " is at " + count.asInt() + "\n");
}
}