Core Concepts
Production Readiness
Soklet is designed to be a small HTTP/1.1 application server that you run behind production edge infrastructure. It owns request routing, response writing, streaming, SSE, MCP transport, lifecycle hooks, and metrics. It intentionally does not own every platform concern.
Deployment Boundary
Run Soklet behind a load balancer, ingress, or reverse proxy that handles internet-facing transport policy:
- Terminate TLS at the edge
- Speak HTTP/2 or HTTP/3 to clients if you need those protocols
- Forward HTTP/1.1 to Soklet
- Enforce coarse connection, IP, WAF, and rate-limit policy before requests reach the JVM
- Preserve
Host,Forwarded, and trustedX-Forwarded-*headers if your application uses effective-origin or effective-client-IP resolution
Soklet does not provide in-process TLS termination, HTTP/2, HTTP/3, or WebSockets. For server push, use Server-Sent Events. For MCP clients, use Soklet's MCP POST, GET, and DELETE transport support.
For browser-facing SSE, prefer an edge that speaks HTTP/2 or HTTP/3 to clients even though it forwards HTTP/1.1 to Soklet. Browsers enforce low per-origin connection limits for HTTP/1.x, and long-lived SSE streams can consume those slots. HTTP/2 and HTTP/3 multiplex many browser-side streams over fewer client-to-edge connections, which avoids SSE starving ordinary page/API traffic while keeping Soklet's backend protocol simple.
If Soklet is directly reachable by untrusted clients, configure origin and forwarded-header handling defensively. Use TrustPolicy.TRUST_NONE unless the forwarding proxy is under your control, and use CORS allowlists rather than permissive origin reflection. For proxied client IPs, prefer EffectiveClientIpResolver with TrustPolicy.TRUST_PROXY_ALLOWLIST so client-supplied X-Forwarded-For values are only trusted through known proxy hops.
Timeouts And Backpressure
Production configs should set explicit limits instead of relying only on defaults:
requestHeaderTimeoutis a transport-layer read bound for the request line and headers. Lower values strengthen slow-client and slow-loris protection.requestBodyTimeoutis a transport-layer read bound for the complete request body after headers have been received. It is a total body-phase timeout, not an idle-progress timeout. It applies to standard HTTP and MCP; SSE handshakes do not accept request bodies.responseWriteIdleTimeoutis a standard HTTP write-side idle bound for non-streaming responses. It protects fixed-length and file responses from stalled readers after the request has completed. The default is 60 seconds; set it toDuration.ZEROto disable this timeout.requestHandlerTimeoutbounds application handler execution. For HTTP it covers the resource method and response marshaling. For SSE it covers the handshake handler. For MCP it covers JSON-RPC handler execution, includingMcpEndpoint::initialize. This is the knob to raise for long-running handlers.requestHandlerConcurrencyandrequestHandlerQueueCapacitybound active and queued handler work.maximumRequestSizeInBytesrejects oversized request lines, headers, framing, and bodies before application code sees them. It is also the per-connection buffering bound while a request is being read, so it directly controls worst-case pre-dispatch memory; lower it to the smallest value your API needs.maximumHeaderCount,maximumHeadersSizeInBytes, andmaximumRequestTargetLengthInBytesreject request-shape attacks that can otherwise stay under a total byte-size ceiling.writeTimeoutbounds SSE and MCP stream writes. The default is 30 seconds; set it toDuration.ZEROonly when slow or stalled stream clients should be allowed to remain connected indefinitely.streamingResponseTimeoutandstreamingResponseIdleTimeoutbound HTTP streaming producers.shutdownTimeoutcontrols how long Soklet waits for server executors to drain before interrupting stragglers.
Treat queue capacity as a memory and latency budget, not just a throughput knob. A large queue can absorb bursts, but it can also hide overload and increase tail latency. In most production systems, a bounded queue plus fast 503 Service Unavailable is preferable to unbounded request accumulation.
These settings are configured on the transport builders before the transport is added to SokletConfig. The setting names above match builder method names where the setting applies.
Builder references:
- Standard HTTP:
HttpServer.Builder - SSE handshakes and streams:
SseServer.Builder - MCP requests and MCP SSE streams:
McpServer.Builder
See Server Configuration for complete examples showing where these builder methods are set.
Secure Defaults Audit
Soklet's built-in defaults are intentionally bounded for request shape, slow-client protection, and connection counts. Treat the table below as the audit checklist for safe out-of-the-box posture versus production overrides.
| Surface | Default posture | Production decision |
|---|---|---|
| Standard HTTP request parsing | 60s header/body timeouts, 10 MB maximum request size, 100 headers, 64 KB header section, 8 KB request target | Lower limits to the smallest values your API needs |
| Standard HTTP response writes | 60s non-streaming response write-idle timeout | Keep enabled; lower it if stalled readers should be reaped faster |
| Standard HTTP connection count | concurrentConnectionLimit(8_192) | Tune the cap below the process file-descriptor limit and below the edge/proxy connection budget |
| HTTP streaming responses | 1 MB producer queue per stream, 16 KB chunks, streaming idle timeout defaults to requestBodyTimeout, total stream timeout disabled | Use streamingResponseIdleTimeout for stalled producers; add a total timeout only if streams should have a maximum lifetime |
| SSE handshakes and streams | 60s handshake timeouts, 64 KB maximum handshake request, 64 KB header section, bounded queues/caches, concurrentConnectionLimit(8_192), 30s stream writeTimeout | Tune the connection cap to your FD budget and lower or disable writeTimeout deliberately |
| MCP transport | 60s request timeouts, 10 MB maximum request size, 64 KB header section, browser-originated requests rejected by default, concurrentConnectionLimit(8_192) for live GET streams, 30s stream writeTimeout, in-memory sessions expire after 24 hours idle | Tune concurrentConnectionLimit and writeTimeout, and use a shared session store plus sticky routing for multi-node deployments |
The 8192 connection defaults are protective ceilings, not universal production recommendations. Internet-facing production systems should still size them deliberately, because the right values depend on your ulimit -n, pod/container density, long-lived stream count, and edge rate-limit policy. concurrentConnectionLimit(0) disables Soklet's HTTP, SSE, and MCP caps when an embedder intentionally delegates connection limiting to another layer.
Recommended Defaults By Workload
A starting-point table for three common archetypes. Tune from here based on observed queue depth, p99 latency, and saturation behavior.
| Setting | JSON API | MCP server | Streaming-heavy |
|---|---|---|---|
requestHeaderTimeout | 5s | 5s | 5s (handshake only) |
requestBodyTimeout | 30s | 30s | not applicable |
responseWriteIdleTimeout | 30s | not applicable | 30s for non-streaming responses |
requestHandlerTimeout | 10s | 120s | 5s (handshake) |
requestHandlerConcurrency | 2× CPU | 4× CPU | 1× CPU |
requestHandlerQueueCapacity | 200 | 200 | 100 |
maximumRequestSizeInBytes | 1 MB | 10 MB | 1 MB |
maximumHeaderCount | 100 | 100 | 100 |
maximumHeadersSizeInBytes | 64 KB | 64 KB | 64 KB |
maximumRequestTargetLengthInBytes | 8 KB | 8 KB | 8 KB |
concurrentConnectionLimit | finite HTTP connection cap, often lower than the 8192 default | finite MCP stream cap, often lower than the 8192 default | finite SSE stream cap, often lower than the 8192 default |
shutdownTimeout | 30s | 30s | 30s |
writeTimeout | not applicable | 30s | 30s |
streamingResponseTimeout | not applicable | not applicable | unset (or large) |
streamingResponseIdleTimeout | not applicable | not applicable | 60s |
Notes:
- Concurrency multipliers assume virtual-thread executors. For platform-thread pools, start lower (1× CPU) and rely on the queue.
shutdownTimeoutshould match the deployment platform's grace period. See the Kubernetes recipe in Deployment Recipes for theterminationGracePeriodSecondsrelationship.- Long-running MCP tool calls (LLM, RAG retrieval, external tool execution) drive
requestHandlerTimeouthigher than typical web APIs; size it to the worst-case tool latency you actually want to accept. LeaverequestHeaderTimeouttight to preserve slow-client protection. Raising read timeouts does not help long handler execution because handler work is governed byrequestHandlerTimeout. - Large or slow uploads may need a larger
requestBodyTimeoutbecause it bounds total body-read time even when bytes continue arriving. streamingResponseTimeoutis intentionally left unset for streaming-heavy workloads; rely onstreamingResponseIdleTimeoutto disconnect stalled clients without bounding total stream lifetime.- Size connection limits from the bottom up: reserve file descriptors for outbound clients, files, logs, and the JVM, then allocate the remainder across HTTP, SSE, and MCP ports. The edge should usually reject or queue before Soklet reaches its own cap.
Threading Model
Soklet uses transport threads for network mechanics and separate executors for application work:
- HTTP resource methods and response marshaling run on the HTTP request-handler executor.
- HTTP
StreamingResponseBodyproducers run on the HTTP streaming executor, so long-lived streams do not occupy request-handler threads. - SSE handshakes run on the SSE request-handler executor.
- Established SSE connections are processed on a separate connection executor;
SseBroadcaster::broadcastEventenqueues work and returns without performing socket writes inline. - MCP JSON-RPC handling runs on the MCP request-handler executor, including framework-managed
McpEndpoint::initialize. - MCP
GETSSE streams run on the MCP connection executor. - Lifecycle observers and metrics collectors are called on the thread performing the observed operation.
Default HTTP and MCP executors use virtual threads when the runtime supports them and fall back to bounded platform-thread pools otherwise. SSE specifically requires virtual-thread support: the SSE server refuses to start on a runtime without virtual threads. You can supply custom executor services through the server builders when you need to integrate with an application-specific executor policy.
Lifecycle observers and metrics collectors are hot-path callbacks. Some are invoked from selector, accept-loop, and stream-processing threads, so blocking them can stall socket I/O or connection cleanup. Keep them thread-safe, non-blocking, and failure-contained. Do not perform network I/O, blocking exports, or heavyweight logging directly inside those callbacks.
Process Configuration
JVM Flags
A reasonable production baseline for Java 21+:
-Xmx2g
-XX:+UseZGC -XX:+ZGenerational
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dump.hprof
-XX:+ExitOnOutOfMemoryError
-Xmx2g
-XX:+UseZGC -XX:+ZGenerational
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap-dump.hprof
-XX:+ExitOnOutOfMemoryError
Notes:
- Generational ZGC suits Soklet's typical workload mix of many short HTTP requests plus long-lived SSE/MCP streams. Sub-millisecond pauses keep streaming connections from being starved during collection.
- Set
-Xmxexplicitly. The JVM autodetects container memory limits, but explicit sizing keeps heap behavior deterministic across base-image and runtime updates. -XX:+ExitOnOutOfMemoryErroris preferable for orchestrated deployments. The orchestrator restarts the pod cleanly rather than leaving a degraded JVM running with intermittent failures.- For CPU-constrained environments, also set
-XX:ActiveProcessorCount=Nto match the container's CPU limit. The JVM otherwise sizes its internal pools to the host's CPU count, not the container's.
File Descriptor Limits
Long-lived SSE and MCP streams hold one file descriptor per connection. Default container limits (often 1024) saturate at modest concurrency. Verify and raise:
ulimit -n 65536
ulimit -n 65536
Container images that run the JVM as PID 1 often inherit unhelpful defaults. Verify the effective limit inside the container with cat /proc/$$/limits.
Kubernetes does not expose RLIMIT_NOFILE as a portable per-pod field — securityContext.sysctls configures kernel /proc/sys parameters, not per-process resource limits. Practical options:
- Bake the limit into your container image entrypoint (e.g., a wrapper script that calls
ulimit -n 65536beforeexec java ...). - Configure your container runtime's default ulimits at the node level (containerd
default_ulimits, Docker--default-ulimit). - Use a platform-specific extension if your distribution offers one.
Graceful Shutdown
Stopping a Soklet instance first stops accepting new connections. Standard HTTP closes idle keep-alives immediately, lets already-dispatched handlers finish, writes their responses with Connection: close, and then force-closes remaining HTTP connections at shutdownTimeout. Active SSE and MCP streams are terminated with StreamTerminationReason.SERVER_STOPPING. Server executors are asked to shut down gracefully so already-queued request, handshake, and stream-write work can complete within the same shutdown budget. If work is still running after the deadline, Soklet interrupts the remaining executor tasks.
Streaming and producer code should cooperate with shutdown and client disconnects:
- HTTP streaming producers should poll
CancelationToken::isCanceled, registerCancelationToken::onCancel, or callCancelationToken::throwIfCanceled. - SSE and MCP stream observers should use the paired
willTerminate...anddidTerminate...lifecycle callbacks for cleanup, metrics, and audit events. - Long-running code should preserve interrupt status when it catches
InterruptedException.
Observability
Use LifecycleObserver for detailed event hooks and request/stream tracing. Use MetricsCollector for counters, gauges, and histograms. The default collector can expose Prometheus/OpenMetrics-compatible text. If your platform standardizes on OpenTelemetry, use soklet-otel for both metrics and spans.
Soklet parses inbound W3C traceparent and tracestate headers into Request::getTraceContext. Core Soklet does not create spans, make sampling decisions, generate child span IDs, or install ambient tracing scope. Application code can read the parsed context directly for logs and outbound clients. OpenTelemetryLifecycleObserver uses that parsed context as the remote parent for emitted OpenTelemetry spans.
Keep trace IDs out of metric labels. Trace IDs belong in spans and logs; metric labels should stay low-cardinality. If you need metrics-to-trace drill-down, use OpenTelemetry exemplars instead of adding trace IDs to counters or histograms.
Exposing Metrics For Scraping
For self-hosted scraping with the default in-memory collector, expose a resource method:
@GET("/metrics")
public MarshaledResponse metrics(@NonNull MetricsCollector metricsCollector) {
String body = metricsCollector.snapshotText(
SnapshotTextOptions.fromMetricsFormat(MetricsFormat.PROMETHEUS)
).orElse(null);
if (body == null)
return MarshaledResponse.fromStatusCode(204);
return MarshaledResponse.withStatusCode(200)
.headers(Map.of("Content-Type", Set.of("text/plain; charset=UTF-8")))
.body(body.getBytes(StandardCharsets.UTF_8))
.build();
}
@GET("/metrics")
public MarshaledResponse metrics(@NonNull MetricsCollector metricsCollector) {
String body = metricsCollector.snapshotText(
SnapshotTextOptions.fromMetricsFormat(MetricsFormat.PROMETHEUS)
).orElse(null);
if (body == null)
return MarshaledResponse.fromStatusCode(204);
return MarshaledResponse.withStatusCode(200)
.headers(Map.of("Content-Type", Set.of("text/plain; charset=UTF-8")))
.body(body.getBytes(StandardCharsets.UTF_8))
.build();
}
See Metrics Collection for filter/format options, OpenMetrics output, and OpenTelemetry-backed collectors. OpenTelemetryMetricsCollector exports through OTel's pipeline and does not require a /metrics endpoint.
At minimum, production dashboards should cover:
- Accepted and rejected connections by server type
- Low-level transport failures across HTTP, SSE, and MCP, especially write-idle timeouts, write timeouts, accept-loop failures, and event-loop task failures
- Request read failures and request-handler rejections
- HTTP response counts and durations by route
- Streaming, SSE, and MCP stream terminations by reason
- Active SSE clients, active MCP sessions, and active MCP SSE streams
- MCP JSON-RPC outcomes and request durations by endpoint and method
Multi-Node Deployments
SSE broadcasters are node-local. A broadcast on one Soklet node only reaches clients connected to that node. For clustered SSE, publish domain events to a shared queue or pub/sub system and let each node rebroadcast locally. If you support Last-Event-ID catch-up, store replay data in a shared durable log.
MCP session metadata can use a custom shared McpSessionStore, but live MCP GET streams are still node-local. Route all requests for a given MCP-Session-Id to the node that owns that stream, commonly through consistent hashing or explicit affinity at the edge.
Health Checks
Expose an application-level readiness endpoint that verifies the dependencies your resource methods require. For simple transport-level health, Soklet also supports OPTIONS *, which lets a load balancer query server-wide capabilities without targeting an application route.
Keep readiness and liveness separate. A process can be alive while temporarily not ready to receive traffic because a dependency is unavailable, migrations are running, or a graceful shutdown has started.
Static Files
For bundled web assets, prefer StaticFiles over hand-rolled path joins. Keep the configured root read-only during normal operation and publish changes atomically.
For high-volume public assets, many production systems should serve from object storage plus a CDN, such as S3 plus CloudFront or an equivalent setup, rather than sending every asset request through Soklet. Use Soklet for assets that are naturally app-owned, bundled with the deployment, or need application-local routing and policy.
When Soklet does serve files, use edge or CDN caching where appropriate. Configure Soklet cache policy deliberately: long-lived immutable caching for fingerprinted assets, and revalidation or no-cache for HTML and other frequently changing entrypoints.
The default weak metadata ETag is cheap and deterministic across nodes serving the same filesystem metadata. EntityTagResolver::fromContentHash provides strong ETags, but it reads the full file on the request-handling thread, including for HEAD. Use manifest-backed ETags for large files or HEAD-heavy traffic.
Do not follow symlinks unless the deployment requires it. Add X-Content-Type-Options: nosniff through headersResolver when serving browser-facing assets, and use accessResolver for path- or attribute-based hide/deny policy. Request-aware authorization should happen before calling StaticFiles.
See Static Files for routing examples, resolver configuration, MIME defaults, cache validators, and range behavior.
Deployment Recipes
These are starting points. Tune sizes, timings, and flags to your workload and platform.
Dockerfile
A minimal production-shaped image. The exec form on ENTRYPOINT ensures the JVM receives SIGTERM directly so graceful shutdown engages.
FROM amazoncorretto:25
ENV JAVA_OPTS="-Xmx2g \
-XX:+UseZGC -XX:+ZGenerational \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp \
-XX:+ExitOnOutOfMemoryError"
# Application artifact
COPY build/libs/myapp.jar /app/myapp.jar
EXPOSE 8080
USER 1000
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/myapp.jar"]
FROM amazoncorretto:25
ENV JAVA_OPTS="-Xmx2g \
-XX:+UseZGC -XX:+ZGenerational \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp \
-XX:+ExitOnOutOfMemoryError"
# Application artifact
COPY build/libs/myapp.jar /app/myapp.jar
EXPOSE 8080
USER 1000
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/myapp.jar"]
This recipe assumes myapp.jar is already built. If your container image builds Java source, make sure your build invokes Soklet's annotation processor, for example through Maven or Gradle annotation processing, or direct javac -processor com.soklet.SokletProcessor.
For a more complete buildable example, see the barebones-app Docker recipe.
Kubernetes Deployment
A starting-point manifest. Pay particular attention to the terminationGracePeriodSeconds / shutdownTimeout relationship and the preStop hook.
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
# Must exceed Soklet's shutdownTimeout. K8s sends SIGKILL after this elapses.
terminationGracePeriodSeconds: 35
containers:
- name: myapp
image: myorg/myapp:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "2Gi"
lifecycle:
preStop:
# Let the load balancer remove this pod from rotation
# before the JVM begins shutting down.
exec:
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
# Must exceed Soklet's shutdownTimeout. K8s sends SIGKILL after this elapses.
terminationGracePeriodSeconds: 35
containers:
- name: myapp
image: myorg/myapp:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "2Gi"
lifecycle:
preStop:
# Let the load balancer remove this pod from rotation
# before the JVM begins shutting down.
exec:
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
Key invariants:
terminationGracePeriodSecondsmust be greater than Soklet'sshutdownTimeout, with a few seconds of buffer. K8s sendsSIGKILLafter the grace period; if Soklet is still draining, in-flight requests get cut.- The
preStop sleeplets the service mesh or ingress controller observe the pod'sTerminatingstate and remove it from rotation before the JVM starts shutting down. Without it, a small number of requests will hit a draining pod and 503. - Readiness and liveness should target distinct paths and have different failure semantics. Readiness can fail temporarily without restarting the pod (dependency unavailable, migrations running, graceful shutdown started); liveness failures restart the container.
- For MCP-heavy workloads, configure ingress for session affinity by
MCP-Session-Idheader. Live MCPGETSSE streams are node-local; non-sticky load balancing breaks session continuity. See Multi-Node Deployments.
Service And Ingress
A standard Service exposing the deployment, plus your ingress of choice (nginx, ALB, Envoy, Cloudflare). The ingress is responsible for TLS termination, HTTP/2 or HTTP/3 negotiation with clients, WAF, rate limiting, and forwarded-header propagation. See Deployment Boundary.
Common Pitfalls
terminationGracePeriodSecondsshorter thanshutdownTimeout. Kubernetes sendsSIGKILLbefore Soklet finishes draining. Pods drop in-flight requests. Verify the grace period exceedsshutdownTimeoutwith at least 5 seconds of buffer.- Default
ulimit -nin containers. Many SSE/MCP connections saturate the per-process file-descriptor limit at modest concurrency. Raise to 65536 or higher. - Blocking I/O inside lifecycle observers or metrics collectors. Hot-path callbacks can run on selector, accept-loop, and stream-processing threads. Blocking exports, network I/O, or heavyweight logging here back-pressure request handling and connection cleanup. Buffer and flush asynchronously.
- Trace IDs as metric labels. Cardinality explosion. Use OpenTelemetry exemplars for metrics-to-trace drill-down instead.
- Reflecting
Origininstead of allowlisting. Permissive CORS in production. See CORS for safe defaults. - MCP
GETSSE streams without sticky routing. Streams are node-local; non-sticky load balancing breaks session continuity. Route byMCP-Session-Idheader at the edge. - Assuming the default connection cap matches your FD budget. HTTP connections, SSE streams, and MCP live streams default to 8192 concurrent connections. That is safer than unlimited, but it may still exceed a container or development host's file-descriptor budget after logs, static files, outbound clients, and JVM internals are reserved. Tune lower when needed.
concurrentConnectionLimit(0)intentionally disables the HTTP, SSE, and MCP caps. maximumRequestSizeInBytessized for bodies you never receive. Each connection may buffer up to this limit while a request is being read, so worst-case pre-dispatch memory is the connection cap multiplied by the request-size cap; the 8192-connection and 10 MB defaults allow a large theoretical ceiling under deliberate abuse. KeepmaximumHeadersSizeInBytestight for request metadata and lowermaximumRequestSizeInBytesto the smallest body size your API needs (1 MB or less is typical for JSON APIs), or enforce tighter request-size limits at the edge.- Clients that send
Expect: 100-continue. Soklet rejects any request bearing anExpectheader with400 Bad Requestbefore reading the body; interim100 Continueresponses are not yet supported. curl addsExpect: 100-continueautomatically for larger request bodies (disable it with-H 'Expect:'), and most reverse proxies (including nginx) handle the100-continueexchange with the client themselves and forward the body without the header, so LB-fronted deployments are typically unaffected. - Disabling stream write timeouts for untrusted clients. SSE and MCP
writeTimeout(Duration.ZERO)permits slow stream readers to stay connected indefinitely. Keep the default 30 second timeout, or another nonzero timeout, when clients are not fully trusted or when streams are high-volume. - Request-time hashing of large static files.
EntityTagResolver::fromContentHashreads the whole file on the request-handling thread, including forHEAD. Use metadata ETags or a manifest-backed resolver for large files. - Unbounded
requestHandlerQueueCapacity. Hides overload as growing tail latency rather than surfacing it as fast503 Service Unavailable. Prefer a small queue. - Missing
-Xmxin containers. The JVM autodetects container memory but explicit sizing keeps heap behavior deterministic across base-image and runtime updates. - Shell-form
ENTRYPOINTorCMDin Dockerfiles. Wraps the JVM in a shell that swallowsSIGTERM; graceful shutdown never engages. Useexecform orexec sh -c "exec java ...".

