Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction to Sketchboot

Version: 1.0.0

Welcome to the official documentation for Sketchboot, the ultra-fast, fixed-memory (1 MB) rate limiting and heavy-hitter detection starter for Spring Boot 3.x.

What is Sketchboot?

Sketchboot is a bridge between the blazing-fast performance of Rust and the enterprise robustness of Java. By utilizing the cutting-edge Java 22 Foreign Function & Memory (FFM) API, Sketchboot embeds a Rust-based Count-Min Sketch (cl-tds) directly into your Spring Boot application’s native memory.

Why was Sketchboot built?

Traditional rate limiters (like Redis, Caffeine, or Bucket4j) often suffer from:

  1. Memory Bloat: The more unique users or IP addresses you track, the larger the memory footprint grows. A million unique IPs can consume hundreds of megabytes of JVM Heap.
  2. Garbage Collection Pauses: High object creation (like tracking maps or buckets) puts pressure on the Java Garbage Collector, causing latency spikes in production.
  3. Network Latency: External rate limiters like Redis require a network hop for every single request.

The Sketchboot Advantage

  • Fixed O(1) Memory: Sketchboot uses a probabilistic data structure (Count-Min Sketch). Whether you have 10 users or 100 Million users, the memory footprint remains locked at exactly 1 Megabyte.
  • Zero JVM Heap: The state is allocated off-heap using the FFM API. This means zero Garbage Collection pressure.
  • Microsecond Latency: Rate limits are checked natively in RAM without any network calls, taking only ~0.005 milliseconds per request.
  • Cross-Platform: Sketchboot ships with pre-compiled native binaries for Linux (.so), Windows (.dll), and macOS (.dylib). It works automatically out of the box.

In the next sections, we will explore how to install and use Sketchboot.

Getting Started

This guide will walk you through integrating Sketchboot v1.0.1 into your Spring Boot application.

Prerequisites

Before using Sketchboot, ensure your system meets the following requirements:

  • Java Version: JDK 22 or higher (Sketchboot uses the Java 22 FFM API).
  • Spring Boot: Version 3.2.x or 3.3.x.
  • Build Tool: Maven or Gradle.

1. Add the Dependency

Sketchboot is published on Maven Central. Add the following dependency to your pom.xml:

<dependency>
    <groupId>io.github.ddsha441981</groupId>
    <artifactId>sketchboot-spring-boot-starter</artifactId>
    <version>1.0.1</version>
</dependency>

For Gradle (build.gradle):

implementation 'io.github.ddsha441981:sketchboot-spring-boot-starter:1.0.1'
⚠️ CRITICAL WARNING: Do not use v1.0.0 in Production

If you are migrating or upgrading, please note that v1.0.0 contains a critical performance bug. SpEL expressions (like #userId) were being dynamically parsed on every single HTTP request. Under high traffic, this causes massive CPU spikes and Garbage Collection (GC) overhead, completely destroying the "Zero GC" benefits of the native Rust engine.

Always use v1.0.1 or higher. In v1.0.1, the SpEL ASTs are safely cached in memory, guaranteeing absolute zero-GC overhead and true lock-free execution.

2. Enable Native Access

Because Sketchboot calls a native Rust library via FFM, you must tell the JVM to allow native access. If you skip this step, Java 22 will throw an IllegalCallerException.

Add this JVM argument when running your application:

--enable-native-access=ALL-UNNAMED

In Maven (pom.xml)

To run via mvn spring-boot:run, configure the Spring Boot plugin:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <jvmArguments>--enable-native-access=ALL-UNNAMED</jvmArguments>
    </configuration>
</plugin>

In IntelliJ IDEA / Eclipse

Go to your Run/Debug Configurations -> “Modify options” -> “Add VM options” and paste: --enable-native-access=ALL-UNNAMED

3. Protect Your First Endpoint

Simply add the @SketchLimit annotation to any REST controller method.

import io.github.ddsha441981.sketchboot.annotation.SketchLimit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    // Allow 100 requests per 60 seconds.
    // The `#userId` dynamically reads the method parameter.
    @SketchLimit(requests = 100, windowMs = 60000, key = "#userId")
    @GetMapping("/api/data")
    public String getData(String userId) {
        return "Data for " + userId;
    }
}

That’s it! Your endpoint is now secured by a blazing-fast native rate limiter.

Core Concepts & Architecture

Sketchboot works by seamlessly connecting three distinct layers:

  1. Spring AOP (Aspect-Oriented Programming)
  2. Java 22 FFM (Foreign Function & Memory API)
  3. Rust Native Crate (cl-tds)

1. The Native Layer (Rust Count-Min Sketch)

At the lowest level, Sketchboot uses a Rust library called cl-tds. A Count-Min Sketch (CMS) is a probabilistic data structure designed to count frequencies of events in massive data streams.

Instead of keeping an exact count for every single user (like a HashMap<String, Integer>), a CMS uses a fixed-size 2D array of counters and multiple hash functions.

  • Advantage: Memory never grows. It stays at exactly 1 MB regardless of whether you track 10 IPs or 10 Million IPs.
  • Trade-off: There is a tiny, mathematically bounded margin of error (over-counting). In rate-limiting scenarios (like “block at 1000 requests”), dropping a user at 998 instead of 1000 is perfectly acceptable for the massive performance gain.

2. The FFM Bridge

Java 22 introduced the final version of the Foreign Function & Memory API. Sketchboot’s ClTdsNative.java uses this API to load the native shared library (.so, .dll, or .dylib) and map the C-compatible Rust functions directly to Java MethodHandles.

Unlike older technologies (like JNI or JNA), FFM:

  • Does not require writing “glue” C code.
  • Is drastically faster for native invocations.
  • Handles off-heap memory safely via Arena.

3. The Spring Boot Auto-Configuration

When you include sketchboot-spring-boot-starter, Spring Boot’s auto-configuration automatically:

  1. Detects your Operating System (Windows, Linux, or macOS).
  2. Extracts the correct native binary from the jar into a temporary folder.
  3. Loads the binary into the JVM.
  4. Registers SketchAspect.java as a global interceptor for any method annotated with @Sketch*.

Request Lifecycle

  1. User sends a HTTP request to /api/data.
  2. SketchAspect intercepts the call before it hits your controller.
  3. SpEL (Spring Expression Language) evaluates your dynamic key (e.g., #request.remoteAddr).
  4. The Key is hashed into a 64-bit integer.
  5. ClTdsSketch.java passes this hash to the Rust library via FFM.
  6. Rust increments the sketch and returns the new frequency count.
  7. If the count exceeds your limit, SketchThresholdException is thrown (returning a 429 status code).
  8. Otherwise, the request proceeds to your logic.

Annotations Reference

Sketchboot provides 7 distinct annotations to handle various security and rate-limiting use cases. All annotations use the native 1MB sketch backend.

Universal Parameter: key

All annotations accept a key parameter. This is a Spring Expression Language (SpEL) string.

  • By default, if omitted, the key is the hash of the target method’s signature. This applies a global rate limit to the endpoint.
  • You can use #parameterName to limit by a method argument.
  • You can use #request.remoteAddr to limit by IP if HttpServletRequest is a parameter.

1. @SketchLimit

General purpose API rate limiting.

  • Parameters:
    • requests (long): Maximum allowed requests.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Preventing users from spamming an API endpoint (e.g., max 100 API calls per minute).
@SketchLimit(requests = 100, windowMs = 60000, key = "#userId")
public void userDashboard(String userId) { ... }

2. @SketchShield

Defense against intense volumetric attacks (DDoS protection).

  • Parameters:
    • threshold (long): Maximum allowed hits.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Blocking IP addresses that are attempting to flood a specific public endpoint.
@SketchShield(threshold = 5000, windowMs = 10000, key = "#ipAddress")
public void publicPage(String ipAddress) { ... }

3. @SketchFraud

Anti-fraud detection for sensitive events.

  • Parameters:
    • maxEvents (long): Maximum allowed events.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Limiting login attempts, OTP verifications, or credit card validation to prevent brute-force or credential stuffing.
@SketchFraud(maxEvents = 5, windowMs = 300000, key = "#email")
public void attemptLogin(String email, String password) { ... }

4. @SketchSurge

Handling unexpected spikes or errors.

  • Parameters:
    • maxErrors (long): Maximum allowed surges/errors.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Circuit breaking logic. If an endpoint causes more than X errors/retries from a specific client, block further attempts.
@SketchSurge(maxErrors = 50, windowMs = 10000, key = "#tenantId")
public void heavyDatabaseQuery(String tenantId) { ... }

5. @SketchCheat

Gaming and high-frequency action limitation.

  • Parameters:
    • maxActions (long): Maximum allowed rapid actions.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: In gaming or trading platforms, preventing users from firing too many actions (like clicking a button 50 times a second using a macro).
@SketchCheat(maxActions = 10, windowMs = 1000, key = "#playerId")
public void fireWeapon(String playerId) { ... }

6. @SketchHitter

Heavy-hitter detection (Analytics).

  • Parameters:
    • threshold (long): Threshold to be considered a heavy hitter.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Used to identify users or IPs that consume a disproportionate amount of bandwidth or resources.
@SketchHitter(threshold = 1000, windowMs = 3600000, key = "#apiKey")
public void downloadLargeFile(String apiKey) { ... }

7. @SketchSensor

Security Operations Center (SOC) alerting.

  • Parameters:
    • maxAlerts (long): Maximum allowed suspicious activities.
    • windowMs (long): Time window in milliseconds (default: 60000).
  • Use Case: Monitoring scraping bots or anomalous behavior across endpoints.
@SketchSensor(maxAlerts = 20, windowMs = 60000, key = "#botIp")
public void scrapeData(String botIp) { ... }

Advanced Usage & Configuration

Exception Handling

When a rate limit is breached, Sketchboot throws a SketchThresholdException. Sketchboot includes a built-in Spring @ControllerAdvice called SketchExceptionHandler which automatically catches this exception and returns a 429 Too Many Requests HTTP response.

You can override this by defining your own @ExceptionHandler in your application:

@RestControllerAdvice
public class CustomRateLimitHandler {

    @ExceptionHandler(SketchThresholdException.class)
    public ResponseEntity<Map<String, Object>> handleLimit(SketchThresholdException ex) {
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
            .body(Map.of(
                "error", "Custom Rate Limit Exceeded",
                "reason", ex.getMessage()
            ));
    }
}

Micrometer Metrics Integration

Sketchboot automatically integrates with Micrometer. It publishes two key metrics that you can view in Prometheus/Grafana or the /actuator/metrics endpoint:

  1. cltds.query.count: A counter of total requests evaluated by Sketchboot.
  2. cltds.threshold.breach: A counter of how many requests were blocked.

Both metrics have a sketch tag representing the annotation type (e.g., LIMIT, FRAUD, SHIELD), so you can build beautiful Grafana dashboards showing traffic distribution by protection layer.

Dynamic Keys with SpEL

The power of the key parameter lies in SpEL.

By IP Address: If your method accepts HttpServletRequest:

@SketchLimit(requests = 10, key = "#request.remoteAddr")
public void endpoint(HttpServletRequest request) {}

By Complex Object: If you pass a complex object (like a JWT Token or DTO):

@SketchLimit(requests = 5, key = "#payload.user.email")
public void endpoint(@RequestBody UserPayload payload) {}

If the SpEL expression cannot be parsed or evaluates to null, Sketchboot gracefully falls back to a global method-level hash, ensuring your application never crashes due to a rate-limiting configuration error.

Developer & Contribution Guide

If you are a developer looking to contribute to the Sketchboot open-source project, or if you simply want to build it from source, follow this guide.

Project Structure

The project is split into two massive ecosystems:

  1. The Native Library (Rust): The core Count-Min Sketch logic is written in Rust. It compiles to a shared dynamic library.
  2. The Spring Boot Starter (Java): The Java side uses the FFM API to invoke the Rust shared library, wrapped in an easy-to-use Spring Boot AutoConfiguration.
sketchboot/
├── docs/                        # mdBook style documentation
├── rust-cl-tds/                 # (External/Included) The Rust Source Code
├── src/
│   ├── main/java/.../sketchboot # The Core Java FFM implementation
│   │   ├── annotation/          # @SketchLimit, @SketchShield, etc.
│   │   ├── aop/                 # Spring Aspect (SketchAspect.java)
│   │   └── core/                # FFM Bindings (ClTdsNative.java)
│   └── main/resources/          
│       └── natives/             # Pre-compiled OS binaries (.so, .dll, .dylib)
└── pom.xml                      # Maven Build File

Building from Source

1. Requirements

  • JDK 22+
  • Maven 3.8+
  • Cargo & Rust toolchain (If you are modifying the C-bindings)

2. Building the Native Binaries (Optional)

If you make changes to the Rust code, you must recompile for your target OS:

cargo build --release

Then copy the resulting binary (libcl_tds.so, cl_tds.dll, or libcl_tds.dylib) into src/main/resources/natives/.

3. Compiling the Java Project

To compile the Spring Boot Starter:

mvn clean install -DskipTests

This will build sketchboot-spring-boot-starter-1.0.0.jar and install it into your local Maven cache (~/.m2).

Running Tests

Sketchboot comes with an extensive testing suite covering unit tests, integration tests, and simulated heavy-traffic (GodMode) benchmarks.

mvn test

Note: Because tests invoke the native library via FFM, Maven Surefire plugin is configured to run with --enable-native-access=ALL-UNNAMED.

Modifying the FFM Bindings

If the Rust extern "C" signature changes, you must update ClTdsNative.java. Currently, the signature maps to:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn increment_sketch(key: u64) -> u64
}

Which is mapped in Java using Linker.nativeLinker() and FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG).

Publishing a Release

Sketchboot relies on a GitHub Actions CI/CD pipeline (.github/workflows/publish.yml). When a new release tag is pushed, it automatically signs the artifact with GPG and uploads it to Sonatype Maven Central (OSSRH). Make sure to remove -SNAPSHOT from pom.xml before pushing a release.

Batch & Off-Heap Ingestion

Sketchboot is not only designed for individual HTTP rate limiting via annotations. It also provides a low-level, high-throughput ingestion API for background processors, Kafka consumers, and massive data pipelines.

The Problem with Single FFM Calls

While FFM is incredibly fast (sub-microsecond), crossing the Java-to-Rust boundary millions of times per second in a loop still introduces overhead. For streaming data systems where you might receive a batch of 50,000 IPs at once from a message queue, iterating and calling ClTdsNative.SKETCH_INCREMENT individually is suboptimal.

The Solution: Batch Ingestion

Sketchboot’s ClTdsSketch class exposes two advanced methods designed specifically to eliminate FFM boundary overhead for large payloads:

1. incrementBatch(long[] keys)

If you have an array of Java long hashes, you can pass them entirely to Rust in a single FFM crossing. Under the hood, Sketchboot allocates a temporary confined Arena, copies the array into off-heap memory, and tells Rust to process the entire contiguous block at once.

import io.github.ddsha441981.sketchboot.core.ClTdsSketch;

public void processKafkaBatch(long[] userHashes) {
    // sketch is a configured ClTdsSketch instance
    sketch.incrementBatch(userHashes);
}

2. incrementBatchOffHeap(MemorySegment keysSegment, long count)

For the absolute extreme edge cases of performance (e.g., zero-copy parsing engines or direct memory mappers), if your keys are already in off-heap memory, you can pass the raw MemorySegment directly. This avoids JVM array allocation completely and allows Rust to directly read the memory chunk.

public void processDirectMemory(MemorySegment keysSegment, long count) {
    sketch.incrementBatchOffHeap(keysSegment, count);
}

When to use these?

  • Annotations (@SketchLimit): Use for standard HTTP APIs where requests arrive individually.
  • Batch API: Use when processing massive log files, consuming from Kafka, or acting as an aggregator node where thousands of items are grouped together.

Actuator & System Monitoring

Sketchboot seamlessly integrates with Spring Boot Actuator to provide deep visibility into the native Rust memory structures.

1. The /actuator/sketches Endpoint

Sketchboot registers a custom Actuator endpoint that allows you to inspect all currently active Count-Min Sketches in your JVM.

Prerequisites: Ensure you have exposed the endpoint in your application.yml:

management:
  endpoints:
    web:
      exposure:
        include: "health,metrics,sketches"

Fetching All Active Sketches

Request: GET /actuator/sketches

Response:

{
  "LIMIT_60000": "Active (1 MB)",
  "SHIELD_10000": "Active (1 MB)",
  "FRAUD_300000": "Active (1 MB)"
}

Note: Sketches are uniquely identified by their annotation type and time window (e.g., LIMIT_60000 means a @SketchLimit with a 60,000ms window).

Fetching Sketch Details

You can query a specific sketch for its mathematical properties.

Request: GET /actuator/sketches/LIMIT_60000

Response:

{
  "epsilon": 0.0000414,
  "memory_bytes": 1048576,
  "delta_percent": 1.8
}

This tells you that the sketch consumes exactly 1,048,576 bytes (1 MB) of off-heap memory, and has an over-count error bound of ~0.00004.

2. Dynamic Library Extraction Logging

When your application boots up, NativeLibraryLoader attempts to detect your Operating System (Linux, Windows, or Mac) and architecture. It extracts the appropriate binary (e.g., libcl_tds.so) from the .jar to your system’s temp directory and loads it.

If you ever encounter UnsatisfiedLinkError, check the startup logs for:

INFO: Sketchboot loading native library for OS: linux, Arch: amd64
INFO: Successfully extracted and loaded libcl_tds.so from /tmp/sketchboot_native_12345.so

If this fails, it is usually because the tmp folder lacks execution permissions or the JVM lacks --enable-native-access.

Performance Benchmarks

Sketchboot is built on two primary principles: Fixed Memory and Zero-GC execution. This section documents the expected benchmarks of the library.

1. The Memory Guarantee

Traditional HashMap<String, Integer> structures in Java require JVM Object Headers, String allocation overhead, and map entry nodes. Tracking 10,000,000 unique IP addresses in a standard ConcurrentHashMap can easily consume ~500 MB to 1 GB of JVM Heap.

Sketchboot Memory Profile: No matter if you track 1 IP address or 1,000,000,000 (1 Billion) IP addresses, a single Sketchboot bucket consumes exactly 1 Megabyte of native RAM. Because this memory is allocated via Java 22 FFM off-heap, it is invisible to the Garbage Collector. You will experience Zero Stop-The-World (STW) pauses regardless of traffic scale.

2. GodMode Benchmarks

The sketchboot-live-test module contains rigorous local benchmarks.

On a standard developer machine (e.g., i7/16GB RAM), Sketchboot processes requests at extreme speeds:

  • Single FFM Calls (via Spring AOP): The overhead of intercepting an HTTP request, parsing the SpEL key, hashing it, and calling Rust is roughly ~0.005 milliseconds per request. You can comfortably handle >50,000 requests per second per node without the limiter becoming the bottleneck.

  • Batch API Ingestion: When using incrementBatch(long[] keys) to bypass AOP and FFM boundaries, Sketchboot can ingest and count >30 Million items per second.

3. The Auto-Decay Mechanism

Unlike Redis, where you must rely on TTL keys or background cron jobs to clear out old rate-limiting data, Sketchboot handles decay entirely asynchronously in Rust.

When a sketch is initialized with a windowMs, Rust automatically decays the Count-Min Sketch matrix. This occurs natively. There is no Java thread sleeping, no ScheduledExecutorService, and no CPU bloat on the JVM side. The Rust crate intelligently decays counts over time, ensuring that rate limits naturally “cool down” without any application-level intervention.