How I Built a Free Sudoku Generation API with Rust and WebAssembly
Sudoku looks simple — a 9×9 grid, digits 1 through 9, no repeats in any row, column, or 3×3 box. But generating a good puzzle is a genuinely compute-heavy task. It's not "fill the grid and call it a day": you need backtracking search, uniqueness verification, and careful difficulty calibration. When you're serving puzzles through a public API at scale, generation speed stops being a nicety and starts being your latency and your server bill.
I recently shipped a free Sudoku generation API at Sudoku100, and the core engine is written in Rust compiled to WebAssembly. In this post I'll walk through the algorithm, the WASM setup, and the public API design — everything you need to spin up a similar service or just start consuming the endpoint today.
Why WebAssembly for a puzzle generator?
A Sudoku generator has two properties that make it a great fit for WASM:
- It's CPU-bound. Backtracking over 9×9 grids with uniqueness checks is pure number crunching — exactly the kind of tight loop where interpreted JavaScript falls behind compiled code.
- It's portable. The same puzzle logic needs to run in the browser (client-side generation for the interactive game) and on the server (the API endpoint). Write it once in Rust, compile once to WASM, and both surfaces share identical behavior.
The result: one source of truth for puzzle correctness, and near-native speed in both environments.
The algorithm: generate, dig, verify
Generating a valid Sudoku boils down to three steps.
1. Generate a complete solution (backtracking)
Start with an empty grid and fill it cell by cell. At each cell, try candidate digits in random order, recursively continue, and backtrack on dead ends:
fn fill(grid: &mut Grid, pos: usize, rng: &mut impl Rng) -> bool {
if pos == 81 {
return true; // solved
}
let (row, col) = (pos / 9, pos % 9);
let mut candidates: Vec<u8> = (1..=9).collect();
candidates.shuffle(rng);
for n in candidates {
if is_valid(grid, row, col, n) {
grid.set(row, col, n);
if fill(grid, pos + 1, rng) {
return true;
}
grid.set(row, col, 0);
}
}
false
}
Randomizing the candidate order per cell is what guarantees every puzzle comes out different.
2. Dig holes (remove cells)
Once you have a full solution, remove cells according to a difficulty target. The number of remaining clues is the single biggest lever on difficulty:
| Difficulty | Clues remaining |
|---|---|
| Beginner | 40–45 |
| Easy | 35–39 |
| Medium | 30–34 |
| Hard | 25–29 |
| Expert | 20–24 |
| Extreme | 17–19 |
At 17 clues you're brushing up against the theoretical minimum for a puzzle with a unique solution — which is exactly why "extreme" is genuinely extreme.
3. Verify uniqueness
Removing cells can accidentally create puzzles with multiple valid solutions, which is bad UX and technically not a valid Sudoku. The harder tiers run a uniqueness check: if more than one solution exists, re-dig or discard and retry. This is the step that makes "easy" puzzles still satisfying and "extreme" puzzles actually solvable by logic.
Compiling Rust to WASM
The Rust core is compiled with wasm-pack / wasm-bindgen into a sudoku100.wasm binary plus a JS glue file. The build produces artifacts that work in three environments:
wasm_sudoku_standalone.js # browser (ESM)
wasm_sudoku_standalone.cjs # Node.js (CommonJS)
wasm_sudoku_standalone.node.mjs # Node.js (ESM)
Because the same .wasm binary is reused everywhere, the browser game and the server API are guaranteed to produce identical puzzles — no "works on the server but differs in the client" bugs.
On the JS side, the generator exposes a small, clean surface:
import { generateSudokuData, generateSudokuImage } from './sudoku-image-generator-api.js';
const { puzzle, solution } = generateSudokuData({ difficulty: 'medium' });
const png = generateSudokuImage({ difficulty: 'hard', size: 512, onlyImage: true });
The image generator uses the WASM engine to produce the puzzle data, then renders it to a PNG/WebP/JPG buffer via canvas — so you get a ready-to-embed image in one call, not just a data array.
The public API
The endpoint is dead simple: no API key, no auth, no rate-limit ceremony. You just request an image (or data) with a few optional parameters.
Random puzzle (any difficulty):
https://www.sudoku100.com/sudoku-img
A specific difficulty:
https://www.sudoku100.com/sudoku-img/easy
https://www.sudoku100.com/sudoku-img/hard
https://www.sudoku100.com/sudoku-img/extreme
Custom size and format:
https://www.sudoku100.com/sudoku-img?width=800&format=png
https://www.sudoku100.com/sudoku-img?width=720&format=webp
Supported formats: png, webp, jpg. Supported difficulties: beginner, easy, medium, hard, expert, extreme.
A specific puzzle by ID (for linking to a fixed, shareable puzzle):
https://www.sudoku100.com/img-id/238
Embedding a fully interactive game is just an iframe:
<iframe
name="sudoku-embed"
src="https://www.sudoku100.com/embed/interactive"
width="800" height="600"
style="border: none; overflow: hidden; border-radius: 6px;">
</iframe>
Under the hood, the server endpoint (/api/sudoku-image) accepts difficulty, size, format, gridSize (4 for a 4×4 grid, 9 for 9×9), and a returnData/base64 flag that returns JSON instead of raw image bytes — useful for LLM agents or apps that need the puzzle array, not a raster image.
Why this matters for performance
Because generation happens in WASM, a single request can produce a full puzzle and render its image without ever leaving compiled code. The practical result: low per-request latency and no heavy server-side image tooling. For a free API that anyone can hit from a blog, an app, or an AI agent, that cost profile is the whole ballgame — free to run means free to offer.
Try it / use it
The whole thing is open source, including the MCP server and LLM skill integration:
- Repo: sudoku100com-create/Sudoku-generate-API
- Docs & live playground: sudoku100.com/api
If you're building anything that needs on-demand puzzles — a print generator, an app, an AI agent that teaches Sudoku, a daily-challenge site — the API is free and ready. Drop a star on the repo if you find it useful, and feel free to ask questions in the comments.












