Soklet Logo

Core Concepts

Server Configuration

Soklet applications do not deploy on traditional application servers like Jetty or Tomcat. There is no concept of a Servlet Container or a WAR file (although Soklet does offer Servlet integration for legacy code).

Instead, Soklet provides its own HTTP server out-of-the-box: an embedded version of Elliot Barlas' Microhttp in the form of a Server implementation accessible via the Server::withPort builder, which listens to HTTP 1.1 on the port you specify.


Server

The minimum required Server configuration is the port number on which to listen.

Soklet will pick sensible defaults, shown below, for other settings.

// The only required configuration is port number
Server server = Server.withPort(8080 /* port */)
  // Host on which we are listening
  .host("0.0.0.0")
  // The number of connection-handling event loops to run concurrently.
  // You likely want the number of CPU cores as per below
  .concurrency(Runtime.getRuntime().availableProcessors())
  // How long to permit a request to process before timing out
  .requestTimeout(Duration.ofSeconds(60))
  // How long to permit your request handler logic to run
  // (Resource Method + Response Marshaling)
  .requestHandlerTimeout(Duration.ofSeconds(60))
  // Maximum number of request handler tasks that may run concurrently.
  // Defaults to concurrency when virtual threads are unavailable, or concurrency * 16 when they are.
  .requestHandlerConcurrency(Runtime.getRuntime().availableProcessors() * 16)
  // Maximum queued request handler tasks before rejecting with 503.
  // Defaults to requestHandlerConcurrency * 64.
  .requestHandlerQueueCapacity(Runtime.getRuntime().availableProcessors() * 16 * 64)
  // How long to block waiting for the socket's channel to become ready.
  // If zero, block indefinitely
  .socketSelectTimeout(Duration.ofMillis(100))
  // How long to wait for request handler threads to complete on shutdown
  .shutdownTimeout(Duration.ofSeconds(5))
  // The biggest request we permit clients to make (10 MB)
  .maximumRequestSizeInBytes(1_024 * 1_024 * 10)
  // Requests are read into a byte buffer of this size.
  // Adjust down if you expect tiny requests.
  // Adjust up if you expect larger requests.
  .requestReadBufferSizeInBytes(1_024 * 64)
  // The maximum number of pending connections on the socket
  // (values < 1 use JVM platform default)
  .socketPendingConnectionLimit(0)
  // Maximum concurrent connections (0 means unlimited)
  .maximumConnections(0)
  // Request ID generator
  .idGenerator(IdGenerator.defaultInstance())
  // Multipart parser
  .multipartParser(MultipartParser.defaultInstance())
  .build();

// Use our custom server
SokletConfig config = SokletConfig.withServer(server)
  // Not shown: other Soklet builder customizations
  .build();

// Start it up
try (Soklet soklet = Soklet.fromConfig(config)) {
  soklet.start();
  System.out.println("Soklet started, press [enter] to exit");
  soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);
}

Additional notes: requestTimeout controls how long the server waits for request data, while requestHandlerTimeout caps the total time your handler code is allowed to run. requestHandlerConcurrency and requestHandlerQueueCapacity provide backpressure by limiting how many requests can be actively processed or queued. The maximumConnections knob can be used to shed load, and idGenerator controls the values surfaced via Request::getId. Your IdGenerator receives the Request, so you can incorporate request data - for example, X-Amzn-Trace-Id.

Additional defaults not shown above: requestHandlerExecutorServiceSupplier uses a bounded virtual-thread executor when available, and multipartParser defaults to MultipartParser::defaultInstance. If you supply your own requestHandlerExecutorServiceSupplier, Soklet will use that executor and ignore requestHandlerConcurrency and requestHandlerQueueCapacity.

Virtual Threads

The default configuration will transparently use Virtual Threads if available at runtime (JDK 19 or 20 with the --enable-preview flag or JDK 21+ stock configuration) and fall back to native threads if not.

If you prefer not to use Virtual Threads, provide your own ExecutorService to requestHandlerExecutorServiceSupplier as shown above.

References:

Server-Sent Event Server

If your application supports Server-Sent Events, you must also configure a ServerSentEventServer. This is a separate server (and port) dedicated to SSE connections. See the Server-Sent Events documentation for details.

The minimum required configuration is the port number on which to listen.

Soklet will pick sensible defaults, shown below, for other settings.

// The only required configuration is port number
ServerSentEventServer sseServer = ServerSentEventServer.withPort(8081 /* port */)
  // Host on which we are listening
  .host("0.0.0.0")
  // How long to permit an SSE handshake request to process before timing out
  .requestTimeout(Duration.ofSeconds(60))
  // How long to permit your SSE handshake handler logic to run
  .requestHandlerTimeout(Duration.ofSeconds(60))
  // Maximum number of SSE handshake tasks that may run concurrently.
  // Defaults to availableProcessors * 16.
  .requestHandlerConcurrency(Runtime.getRuntime().availableProcessors() * 16)
  // Maximum queued SSE handshake tasks before rejecting with 503.
  // Defaults to requestHandlerConcurrency * 64.
  .requestHandlerQueueCapacity(Runtime.getRuntime().availableProcessors() * 16 * 64)
  // How long to wait when writing SSE data before timing out
  // (0 disables write timeouts)
  .writeTimeout(Duration.ZERO)
  // How often to send heartbeat payloads to keep connections alive
  .heartbeatInterval(Duration.ofSeconds(15))
  // How long to wait for SSE threads to complete on shutdown
  .shutdownTimeout(Duration.ofSeconds(1))
  // The biggest SSE handshake request we permit clients to make (64 KB)
  .maximumRequestSizeInBytes(1_024 * 64)
  // Requests are read into a byte buffer of this size
  .requestReadBufferSizeInBytes(1_024)
  // Maximum concurrent SSE connections (global cap).
  // If exceeded, a 503 is returned via ResponseMarshaler::forServiceUnavailable
  .concurrentConnectionLimit(8_192)
  // Cache sizes for broadcasters (per-resource-path event fanout)
  // and resource path declarations (Route pattern lookups).
  // Increase if you have many distinct SSE URLs in circulation.
  .broadcasterCacheCapacity(1_024)
  .resourcePathCacheCapacity(8_192)
  // Maximum queued SSE writes per connection
  .connectionQueueCapacity(128)
  // Write an initial heartbeat to verify the connection after handshake
  .verifyConnectionOnceEstablished(true)
  // Request ID generator
  .idGenerator(IdGenerator.defaultInstance())
  .build();

SokletConfig config = SokletConfig.withServer(
  Server.fromPort(8080)
).serverSentEventServer(sseServer)
 .build();

Additional notes: requestTimeout and requestHandlerTimeout have the same meaning as the regular server, but apply only to the SSE handshake. requestHandlerTimeout starts when the connection is accepted and includes time spent waiting in the handshake queue. writeTimeout governs how long to allow streaming writes before failing the connection; Duration.ZERO disables write timeouts. requestHandlerConcurrency and requestHandlerQueueCapacity provide backpressure for SSE handshakes (they do not limit active SSE connections).

Additional defaults not shown above: requestHandlerExecutorServiceSupplier uses a bounded virtual-thread executor for handshakes, the request reader uses a separate bounded virtual-thread executor internally, and established SSE connections are processed on a virtual-thread-per-connection executor. If you supply your own requestHandlerExecutorServiceSupplier, Soklet will use that executor and ignore requestHandlerConcurrency and requestHandlerQueueCapacity for the handshake executor.

References:

Previous
Value Conversions