What I Learned Building My First Express Server

I’m primarily a frontend developer. React, Vite, Tailwind etc. that’s my world. The backend has always been that thing I knew existed but rarely touched. Today I finally sat down and built my first Express server from scratch, and I want to document everything that actually clicked, including the things that didn’t at first.

This isn’t exactly a clean tutorial. It’s a honest account of what I learned, what troubled me, and what I’d tell myself if I was starting over.

Setting up from zero

First things first:

“`in your terminal run
npm init -y
npm install express




Then I created `server.js` and wrote the minimum possible server:


const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});


```javascript

Run it with `node server.js`, open `http://localhost:3000` in the browser — "Hello world". That's a working HTTP server in about 10 lines.

Let me break down what each part is actually doing.

---

**Breaking down the code**

### `const express = require('express')`

This pulls the Express library from `node_modules` into the current file. `require` is CommonJS — the default module system in Node. You store it in a variable called `express`.

### `const app = express()`

You call `express()` like a function because it is one. It returns an application object — this `app` is your server's control panel. Everything attaches to it.

### The route


app.get('/', (req, res) => {
  res.send('Hello world');
});

This registers a route. Let’s read it out loud:

“When someone makes a GET request to /, run this function.”

req is the request object — everything the client sent you. Their URL, query params, headers, body.

res is the response object — your tools for sending something back.

app.listen(3000, () => { ... })

This starts the server on port 3000. The callback runs once the server is ready — that’s why you log there and not outside it.

The req/res naming thing

I assumed req and res were special keywords that Express required. They’re not. They’re just parameter names.

Express calls your callback with the request object first and the response object second, always. The names are completely up to you:

// All of these work identically
app.get('/', (req, res) => { res.send('ok') });
app.get('/', (request, response) => { response.send('ok') });
app.get('/', (banana, mango) => { mango.send('ok') });

That last one is valid JavaScript. mango holds the response object and .send() works fine on it.

The community standardized on req and res so everyone reading your code immediately knows which is which. Stick with the convention, but knowing it’s just a convention makes the whole thing less mysterious.

Reading data from the URL

There are two ways a client can send data in a GET request.

URL parameters

app.get(‘/user/:id’, (req, res) => {
const id = req.params.id;
res.json({ userId: id });
});

The colon before id tells Express that this segment of the URL is a variable. So /user/jeffrey, /user/42, and /user/anything all hit this route. Whatever’s in that slot gets stored in req.params.id.

You visit it like: http://localhost:3000/user/jeffrey

For multiple params, it’s a slash between them, not a comma:

app.get(‘/add/:a/:b’, (req, res) => {
// correct
});

app.get(‘/add/:a,:b’, (req, res) => {
// wrong — this will not match the way you expect
});

I made the comma mistake. It cost me time.

Query parameters

app.get(‘/add’, (req, res) => {
const a = Number(req.query.a);
const b = Number(req.query.b);
res.json({ sum: a + b });
});

You visit it like: http://localhost:3000/add?a=10&b=5

When to use which:

URL params are for identifying a specific resource — /user/42, /product/air-max. Query params are better for operations, filters, and optional values — /search?q=express, /add?a=10&b=5. For a math operation, query params are the natural fit.

Splitting logic into modules

Instead of writing all my functions inside the server file, I created a separate math.js:

// math.js
function add(a, b) {
return a + b;
}

function mult(a, b) {
return a * b;
}

module.exports = { add, mult };


Then imported it in the server:

js
// server.js
const { add, mult } = require(‘./math’);


This keeps the server file clean. Routes handle HTTP, modules handle logic. Good habit to build early.

---

** CommonJS vs ESModules**

Node has two module systems and I spent time going back and forth between them today.

**CommonJS:**

js
const express = require(‘express’);
module.exports = { add };


**ESModules:**

js
import express from ‘express’;
export function add(a, b) { return a + b; }


The key differences:

- To use ESM in Node, you need `"type": "module"` in your `package.json`, otherwise Node treats `.js` files as CJS by default
- ESM requires file extensions when importing local files: `./math.js` not `./math`
- You cannot mix them freely. `require` in an ESM file breaks. `import` in a CJS file breaks.

**My recommendation if you're just starting:** stick with CommonJS. No config needed, works out of the box, and the vast majority of Express tutorials and packages are written in it. Switch to ESM when you have a concrete reason to.

---

## Handling POST requests

GET requests carry data in the URL. POST requests carry data in the **body** — like submitting a login form or hitting an API with JSON. You don't want a password showing up in someone's browser history.

js
app.post(‘/login’, (req, res) => {
const { username, password } = req.body;
res.json({ message: Welcome ${username} });
});


But there's a catch. To read `req.body`, you need this line **before** your routes:

js
app.use(express.json());


`app.use()` applies middleware to every incoming request. `express.json()` reads the raw request body and parses it as JSON so you can access it via `req.body`.

I removed this line to see what would happen. Got this:

plaintext
TypeError: Cannot destructure property ‘username’ of ‘req.body’ as it is undefined.


Without it, `req.body` is just `undefined`. The middleware is not optional — and it must come before your routes, not after.

To test POST requests, you can't use the browser URL bar. Use **Thunder Client** (VS Code extension) or Postman. Send a POST to `http://localhost:3000/login` with this JSON body:

json
{
“username”: “jeffrey”,
“password”: “velto123”
}


Response:

json
{
“message”: “Welcome jeffrey”
}

Small things that made me question my sanity

1. The app.listen callback

// Wrong — logs before the server is ready
app.listen(3000, console.log("Server running on port 3000"));

// Right — logs once the server is actually up
app.listen(3000, () => console.log("Server running on port 3000"));

Without the arrow function, console.log executes immediately when the file loads, not when the server is ready. It’s a subtle difference but it matters.

2. res.send() vs res.json()

res.send() is for plain text. res.json() is for objects and structured data. When you’re building an API, you almost always want res.json().

3. Not restarting the server

Every time I made a change and wondered why nothing was different — the old server was still running. Fix this once:

node --watch server.js

Built into Node 18+. No install needed. Your server restarts automatically on every file change.

Where I ended up

By the end of the session I had a working Express server with:

  • GET routes reading from URL params and query params
  • A separate math module with add and mult functions
  • A POST route reading from req.body
  • A solid mental model of how the whole thing fits together

If you’re a frontend developer who’s been avoiding Express the same way I was — it’s genuinely not that far from home. The concepts map to things you already know. A route is just an event listener. Middleware is just a function that runs before your handler. req and res are just objects.

Start with a GET route. Break something. That’s how it sticks.

I’m Chinwuba Jeffrey — frontend developer and CEO of Velto, a premium web design agency focused on the African market. I’m documenting my journey going full-stack. Follow along if you’re on a similar path.

I Build the Infrastructure That Serves AI Models. Gemma 4 Just Made My Job Existential.

Gemma 4 Challenge: Write about Gemma 4 Submission

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

Most posts about Gemma 4 start with “I downloaded it and ran it locally.” Mine starts differently.

I build the platform that serves models like Gemma 4. I spend my days writing Kubernetes manifests, configuring KServe InferenceServices, debugging Knative ingress routes, and making sure Kyverno policies block the bad deployments before they reach the cluster. My project — NeuroScale — is a self-service AI inference platform where a developer fills in a Backstage form, the platform creates a pull request, ArgoCD deploys it, and a production-grade inference endpoint goes live.

I am, in the most literal sense, the person who keeps the lights on for AI model serving.

So when Google released Gemma 4 with models ranging from 2B parameters on a Raspberry Pi to 31B on a workstation, my first thought wasn’t “cool, let me try it.” My first thought was: “Does this make my entire platform unnecessary?”

That question — and the answer I eventually reached — is what this post is about.

The Serving Tax Nobody Talks About

Here’s a dirty secret from platform engineering: most of the cost of running an AI model isn’t the model. It’s everything around it.

On NeuroScale, deploying a single sklearn InferenceService on KServe creates roughly 5 pods. Not one. Five. A predictor pod, a transformer (if configured), an Istio/Kourier sidecar or gateway pod, a queue-proxy injected by Knative, and a storage initializer that downloads model artifacts at startup.

Each of those pods needs CPU requests, memory limits, health probes, and — because we enforce this through Kyverno admission policies — ownership labels and cost-center attribution before they’re even allowed to exist:

# This gets DENIED by our Kyverno policy — no owner, no deploy
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gemma-model
  namespace: default
spec:
  predictor:
    sklearn:
      storageUri: gs://models/gemma/v1
# ❌ Blocked: missing app.kubernetes.io/owner and cost-center labels

On a local k3d cluster — the kind I use for development — a single InferenceService consumes around 1.2 GB of RAM. Add Backstage (the developer portal), ArgoCD (GitOps), Kyverno (policy engine), and OpenCost (cost attribution), and you’re at 6-8 GB of infrastructure memory before you’ve served a single prediction.

This is the serving tax. Every platform engineer pays it. Nobody writes blog posts about it.

Then Gemma 4 E2B Showed Up on a Raspberry Pi

When I read that Gemma 4’s E2B model — 2 billion parameters, multimodal, 128K context window — runs on a Raspberry Pi 5 drawing 7.5 watts, something clicked.

Not “clicked” as in excitement. “Clicked” as in a core assumption quietly becoming wrong.

My entire platform exists because of a specific premise: deploying AI models is hard, dangerous, and requires infrastructure guardrails. The selling point of NeuroScale is that you don’t need to understand Kubernetes to get an inference endpoint. You fill in a form. The platform handles the YAML, the drift correction, the policy enforcement, the cost tracking.

But what if the model just… runs? On the device? With no YAML? No Kubernetes? No platform?

Gemma 4’s model family makes this question concrete in a way previous open models didn’t:

Model Parameters Runs On Needs Platform?
E2B 2B Phone, Raspberry Pi, browser No
E4B 4B Laptop, edge device No
31B Dense 31B Workstation GPU Maybe
26B MoE 26B (4B active) Server Yes

The bottom two rows are my world. The top two rows are… not.

The Three-Hour Outage That Explains Everything

Let me tell you about the worst three hours of my NeuroScale build.

When I first set up KServe, all InferenceService creation was blocked cluster-wide. No model could deploy. Zero. The reason? KServe’s default configuration assumes you’re running Istio as your service mesh. We were running Kourier — a lightweight Envoy-based ingress gateway that uses ~100 MB instead of Istio’s ~1 GB — because our k3d cluster didn’t have the RAM headroom for Istio.

The fix was a single configuration patch:

# infrastructure/serving-stack/patches/inferenceservice-config-ingress.yaml
ingress:
  disableIstioVirtualHost: true

One line. disableIstioVirtualHost: true. That’s what stood between “all inference is blocked” and “everything works.” And this configuration flag isn’t in KServe’s getting-started documentation. I found it by reading the KServe controller source code.

Three hours of production-blocking downtime. One boolean.

Now here’s the thing: if I had been running Gemma 4 E2B locally via Ollama, this outage would not have existed. There’s no ingress to misconfigure. There’s no service mesh. There’s no Kubernetes. There’s just a process listening on a port.

# The entire "platform" for local Gemma 4
ollama run gemma4:e2b
# That's it. No YAML. No CrDs. No three-hour outage.

So why do platforms like mine still exist?

Because “Running a Model” and “Serving a Model” Are Different Problems

This is the nuance I think most Gemma 4 coverage misses.

Running a model means: download weights, load into memory, send a prompt, get a response. Gemma 4 E2B does this beautifully on consumer hardware.

Serving a model means: make it available to 50 developers across 3 teams, ensure the person who deployed it is accountable for its resource cost, prevent someone from accidentally deploying a root container with no memory limits that crashes the shared cluster, automatically roll back when the model artifact URI changes and the new version returns garbage, and produce an audit trail that shows exactly who deployed what, when, and why.

That’s not a model problem. That’s an organizational problem. And Gemma 4 doesn’t solve it — no model does.

On NeuroScale, a single deployment goes through this gauntlet:

Developer fills Backstage form
    → Platform generates YAML with required labels
    → PR is created on GitHub
    → CI runs kubeconform (schema validation)
    → CI runs kyverno-cli (policy simulation)
    → CI calculates resource delta (cost impact)
    → PR is reviewed and merged
    → ArgoCD detects the change
    → ArgoCD applies the manifest
    → Kyverno admission webhook validates live
    → KServe creates the InferenceService
    → Knative provisions the serving infrastructure
    → Smoke tests verify the endpoint responds

Thirteen steps. Most of them invisible to the developer. All of them essential when you’re running models for an organization, not for yourself.

The False-Green That Changed How I Think About AI Governance

Here’s a story that connects Gemma 4’s model selection philosophy to platform engineering in a way I didn’t expect.

For two weeks, our CI pipeline was reporting that all Kyverno policy checks passed. Green checkmarks everywhere. PRs merged with confidence. We thought our guardrails were working.

They weren’t.

The kyverno-cli apply command exits with code 0 even when policies are violated. Zero. Success. The violation details are printed to stdout, but the exit code — the thing CI systems use to determine pass/fail — says “everything’s fine.”

# This exits 0. CI shows green. Policies are NOT enforced.
kyverno-cli apply ./policies/ --resource ./bad-manifest.yaml
echo $?  # 0 ← This is a lie

The correct tool for CI policy validation is kyverno test, which handles exit codes properly and is purpose-built for pipelines. We were using kyverno apply — not out of ignorance, but because it was the first thing in the docs. That was the mistake: reaching for what was convenient instead of verifying it was correct. The fix required checking both the exit code and parsing stdout for violation strings:

# The real check — trust nothing
OUTPUT=$(kyverno-cli apply ./policies/ --resource "$manifest" 2>&1)
EXIT_CODE=${PIPESTATUS[0]}

if [ "$EXIT_CODE" -ne 0 ] || echo "$OUTPUT" | grep -qi "fail|violation|denied"; then
    echo "❌ Policy violation detected"
    exit 1
fi

For two weeks, any developer could have deployed an InferenceService without resource limits, without ownership labels, without cost attribution — and CI would have said “all checks passed.”

This is what governance failure looks like. Not a dramatic breach. A silent green checkmark.

What This Has to Do With Gemma 4

Gemma 4’s model family is the first time I’ve seen model selection treated as a first-class engineering decision rather than an afterthought. The challenge itself asks for “intentional model selection” — show why your model was the right tool for the job.

That phrase — intentional selection — is exactly what was missing from our CI pipeline. We selected kyverno-cli apply because it was the obvious tool. We didn’t verify that it actually enforced what we thought it enforced. The selection was convenient, not intentional.

The same trap exists with Gemma 4’s model variants. E2B is convenient — smallest download, runs anywhere. But if your use case requires multi-step reasoning over 50K tokens of context, the benchmark data shows E4B or 31B Dense is the intentional choice. If your workload needs high-throughput batch processing, the 26B MoE’s 4B active parameters per forward pass is the intentional choice.

Convenience and correctness are not the same thing. I learned that the hard way with a CI tool. Gemma 4’s model family is designed so you don’t have to.

Where Gemma 4 Actually Threatens My Platform (and Where It Doesn’t)

After 108 commits, 21 smoke tests, and 6 milestone postmortems on NeuroScale, here’s my honest assessment:

Gemma 4 Makes My Platform Unnecessary For:

Solo developers and small teams. If you’re one person running one model for your own project, you should be running Gemma 4 locally. Ollama + E2B or E4B, depending on your reasoning needs. No Kubernetes. No KServe. No platform tax. The model runs on your hardware, your data stays on your machine, and you don’t need me.

Edge and on-device inference. The E2B variant running on a phone or Raspberry Pi at 7.5W is a fundamentally different deployment model than anything platform engineers have optimized for. These devices don’t have container runtimes. They don’t need admission controllers. The compute is the deployment. This is a category Gemma 4 created, and it’s outside my scope entirely.

Prototyping and experimentation. When you’re exploring whether Gemma 4 can solve your problem, the correct infrastructure is zero infrastructure. Download the model. Try it. The 128K context window means you can feed it your entire codebase in one shot and ask questions. Don’t set up a serving platform to prototype.

Gemma 4 Makes My Platform More Necessary For:

Multi-team organizations deploying multiple models. The moment you have Team A running a 31B Dense model for code generation and Team B running an E4B for document classification on the same cluster, you need resource isolation, cost attribution, and policy enforcement. That’s not a model capability — it’s an organizational capability.

Regulated environments. Healthcare, finance, government. These industries don’t just need a model that works. They need an audit trail, admission controls, reproducible deployments, and the ability to prove that the model serving configuration hasn’t drifted from what was approved. Gemma 4’s open weights are a regulatory advantage — you control the model — but the serving infrastructure is where compliance lives.

Production SLAs. Knative’s autoscaling, KServe’s canary rollouts, ArgoCD’s self-healing drift correction — these exist because production means “it works at 3 AM on a Sunday when nobody is watching.” Running ollama serve on a workstation is not production. It’s a demo.

The Decision Framework I Wish Existed

Based on my experience building NeuroScale and studying Gemma 4’s architecture, here’s the decision tree for anyone choosing between local inference and platform-served inference:

Question If Yes → If No →
Are you one developer working alone? Local Gemma 4 (E2B/E4B) Keep reading
Is your data too sensitive to leave the device? Local Gemma 4, any variant that fits Cloud/platform OK
Do multiple teams share the same compute? You need a platform Local is fine
Do you need an audit trail for model deployments? You need a platform Local is fine
Does someone need to answer “who deployed this and what does it cost?” You need a platform Local is fine
Is the model serving a production API with SLA guarantees? You need a platform Local is fine

If you answered “no” to all six questions, run Gemma 4 locally and don’t look back.

If you answered “yes” to any of the last four, the model isn’t the hard part. The platform is.

What I’m Actually Building Next

Here’s the honest ending: Gemma 4 didn’t make my platform unnecessary. It made it smaller.

The next milestone for NeuroScale is adding Gemma 4’s 26B MoE as a first-class runtime alongside our existing sklearn InferenceService. The MoE architecture — 26B total parameters but only 4B active per forward pass — has an important nuance: MoE saves compute, not memory. Because the routing mechanism selects experts dynamically per token, all 26B weights must be resident simultaneously. At 4-bit quantization that’s ~13-15GB VRAM. What MoE does give you in a platform context is higher throughput per GPU dollar — the compute overhead per request drops dramatically, so you can serve more concurrent requests on the same hardware. That’s where the resource governance story actually lands.

# What this might look like in NeuroScale
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gemma4-moe-team-alpha
  labels:
    app.kubernetes.io/owner: team-alpha
    cost-center: cc-ml-inference
spec:
  predictor:
    model:
      modelFormat:
        name: vllm
      runtime: gemma4-moe-runtime
      storageUri: gs://neuroscale-models/gemma4-26b-moe
      resources:
        requests:
          cpu: "4"
          memory: "16Gi"     # MoE saves compute, not memory — all 26B weights load at runtime. 16Gi works at 4-bit quant (~13-15GB), but budget for the full model, not just active params.
          nvidia.com/gpu: "1"
        limits:
          cpu: "8"
          memory: "32Gi"
          nvidia.com/gpu: "1"

The E2B and E4B? Those run on developers’ laptops directly. No platform needed. No form to fill. No PR to merge. That’s the right answer for that model tier, and I’m at peace with it.

The platform engineer’s job isn’t to serve every model. It’s to serve the models that need governance, and get out of the way for the ones that don’t.

Gemma 4’s model family, more than any other open model release, forced me to think clearly about that boundary.

The Takeaway

If you’re a developer choosing a Gemma 4 variant, here’s what a platform engineer wants you to know:

  1. E2B and E4B are genuine infrastructure breakthroughs. Not because they’re the smartest models — they’re not. Because they eliminate the serving tax entirely. No Kubernetes. No ingress. No three-hour debugging sessions. Run them locally and ship.

  2. 31B Dense and 26B MoE still need a platform. The moment you need multi-team isolation, cost attribution, or audit trails, the model’s intelligence is the easy part. The infrastructure is where the real engineering happens.

  3. Intentional model selection is governance. Choosing E2B because it’s the smallest download is convenience. Choosing E2B because your task is single-turn classification with sub-second latency requirements on edge hardware — that’s engineering. Gemma 4’s model family is the first open model release where this distinction actually matters at every tier.

  4. The most capable model is not always the right model. I learned this from a CI tool that reported false greens for two weeks. Capability without verification is worse than limited capability you actually validated.

I build platforms that serve AI models. Gemma 4 is the first model family that made me question which models deserve a platform — and that question made my platform better.

NeuroScale is open source: github.com/sodiq-code/neuroscale-platform. 108 commits, 21 smoke tests, 6 milestones of reality-check documentation. The BEFORE.md and AFTER.md tell the full story.

WebAssembly in 2026: The Quiet Revolution That Finally Delivered

WebAssembly in 2026: The Quiet Revolution That Finally Delivered

WebAssembly quietly became production-ready in 2025-2026. The browser wars settled, the toolchain matured, and suddenly WASM is everywhere: in the browser, on the server, at the edge. Here’s what changed and why it matters.

The State of WebAssembly in 2026

The big story: WASM is no longer a niche technology. It’s the runtime for:

  • Edge computing: Cloudflare Workers, Fastly Compute, AWS Lambda@Edge
  • Plugin systems: Extism, wasmtime-based extensions
  • Server-side: Node.js Deno native WASM support, Bun WASM runtime
  • Browser: Every major browser, WebAssembly System Interface (WASI)

The question is no longer “is WASM ready?” It’s “why aren’t you using it?”

What WebAssembly Actually Is

Let’s be precise: WebAssembly is a binary instruction format, not a language. You compile Rust, C++, Go, or other languages to WASM. It’s designed as a portable compilation target.

// This Rust code compiles to WASM
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn fibonacci(n: i32) -> i32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}
// In the browser
WebAssembly.instantiateStreaming(fetch('math.wasm'))
  .then(({ instance }) => {
    console.log(instance.exports.fibonacci(40)); // Fast!
  });

The WASI Story: WebAssembly Outside the Browser

WASI (WebAssembly System Interface) is what made WASM useful outside browsers. It provides a standardized way for WASM modules to interact with system resources (files, network, clocks).

// Rust code using WASI
use std::fs;
use std::io::{self, Read};

fn main() -> io::Result<()> {
    // Read a file (via WASI)
    let mut contents = String::new();
    fs::File::open("input.txt")?.read_to_string(&mut contents)?;

    // Process it
    let word_count = contents.split_whitespace().count();
    println!("Word count: {}", word_count);

    Ok(())
}

This same Rust code runs:

  • In a browser
  • In a Node.js process
  • In a Cloudflare Worker
  • In a standalone WASM runtime

The Rust + WASM Stack in 2026

Setting Up

# Install toolchain
rustup target add wasm32-wasip1
cargo install wasm-pack

# Create a project
cargo new --lib my-wasm-project
cd my-wasm-project

The Code

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn process_data(input: &str) -> String {
    let reversed: String = input.chars().rev().collect();
    format!("Processed: {}", reversed)
}

#[wasm_bindgen]
pub fn calculate_stats(numbers: &[f64]) -> JsValue {
    let sum: f64 = numbers.iter().sum();
    let count = numbers.len() as f64;
    let mean = if count > 0.0 { sum / count } else { 0.0 };

    // Return as JSON object
    serde_wasm_bindgen::to_value(&serde_json::json!({
        "sum": sum,
        "mean": mean,
        "count": count
    })).unwrap()
}

Build and Use

# Build for web
wasm-pack build --target web

# Or for Node.js
wasm-pack build --target nodejs
// In your web app
import init, { process_data, calculate_stats } from './pkg/my_wasm_project.js';

await init(); // Load the WASM module

const result = process_data("Hello WebAssembly!");
console.log(result); // "Processed: !gninrtsnA eb lorem"

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const stats = calculate_stats(numbers);
console.log(stats.mean); // 5.5

Real Performance Numbers

Here’s where WASM actually wins:

// Pure JavaScript
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Benchmark: fibonacci(40)
// JavaScript: ~1200ms
// WASM (Rust): ~0.8ms
// Speedup: ~1500x
Task JavaScript WASM (Rust) Speedup
Fibonacci(40) 1,200ms 0.8ms 1500x
Image resize 4K 450ms 85ms 5.3x
JSON parse 10MB 180ms 42ms 4.3x
AES encryption 95ms 12ms 7.9x

The Component Model: The Missing Piece

The WebAssembly Component Model (WCM) shipped in 2025-2026 and is the biggest thing to happen to WASM. It allows WASM modules to compose and interface with each other without shared memory.

// my-component.wit
interface math-utils {
  add: func(a: f64, b: f64) -> f64;
  multiply: func(a: f64, b: f64) -> f64;
}

world my-component {
  export math-utils;
}
// implementation
use wit_bindgen::generate;

generate!({
    world: "my-component",
    exports: {
        "math-utils": MathUtils,
    }
});

struct MathUtils;

impl exports::math_utils::Guest for MathUtils {
    fn add(a: f64, b: f64) -> f64 { a + b }
    fn multiply(a: f64, b: f64) -> f64 { a * b }
}

The component model means you can:

  • Mix Rust, Go, C++ components in one application
  • Version components independently
  • Share interfaces between languages

Where It’s Being Used in Production

Figma (Since 2017)

The original success story. Their creative tool runs in the browser at near-native speed using C++ compiled to WASM.

Google Earth (2025 Rewrite)

Rewrote their 3D rendering engine in Rust + WASM, deployed to browser and desktop.

Cloudflare Workers

2 million+ developers deploy to 300+ data centers using WASM-based edge functions. Your JavaScript runs in a WASM sandbox.

Photoshop (Web Version)

Adobe’s web port uses WASM for performance-critical image operations. You get real Photoshop tools in a browser.

AI Inference

WASM-native ML inference frameworks (wasmML, ONNX Runtime WASM) are enabling client-side AI. Llama.cpp compiled to WASM runs 7B parameter models in browsers.

The Toolchain Maturity Story

Before 2024

  • Toolchains were inconsistent
  • Debugging was painful
  • Component Model didn’t exist
  • WASI was immature

After 2026

  • wasm-pack, cargo-component, wit-bindgen are production-ready
  • Chrome DevTools has native WASM debugging
  • WASI Preview 2 is stable
  • Component Model is shipping everywhere
# The modern workflow is now straightforward
cargo add wasm-bindgen serde serde_json --target wasm32-wasip1

wasm-pack build --target web --release

# Deploy to npm, use in your build step

When to Use WebAssembly

Use WASM when you need:

  • Performance-critical computations (image processing, cryptography, scientific computing)
  • Portability across environments (browser, server, edge)
  • Sandboxed plugin systems
  • Running existing C/Rust code in browser

Don’t use WASM when:

  • You need DOM manipulation (stick with JS)
  • Your code is I/O bound, not CPU bound
  • Simple UI logic (WASM overhead isn’t worth it)
  • You need massive ecosystem packages (npm works fine)

The Extism Framework: Plugins in Your App

Extism is the framework for building plugin systems with WASM. You write plugins in any language, run them in your host application.

// My plugin (Rust)
use extism_pdk::*;

#[plugin_fn]
pub fn transform(input: String) -> FnResult<String> {
    let result = input.to_uppercase();
    Ok(result)
}
// Host application (C#)
using Extism;

var manifest = new Manifest(
    WasmFile: "transform.wasm"
);

using var plugin = new Plugin(manifest);
var result = plugin.Call("transform", "hello world");
// result = "HELLO WORLD"

The plugin runs in a WASM sandbox, isolated from the host. You can load untrusted code safely.

My Recommendation

Start with WASM when:

  1. You have a performance bottleneck in JavaScript that profiling confirms
  2. You need to run code in multiple environments (browser + edge + server)
  3. You want a safe plugin system for user-provided code

Start with Rust + WASM if:

  • You’re building performance-critical infrastructure
  • You want maximum portability
  • You’re already a Rust shop

The learning curve is real but the tooling is finally good enough that you don’t need to be a WASM expert to ship production code.

Using WebAssembly in production? What’s your stack and what problems did it solve?

Here’s a beginner-friendly guide to GitHub, especially focused on forks and pull requests, which are the core of real-world collaboration

🚀 GitHub for Beginners (Forks, Pull Requests, and Collaboration)

GitHub is where developers store code, collaborate, and contribute to projects. If Git is the engine, GitHub is the social platform built on top of it.

Let’s break it down simply.

🌍 1. What is GitHub?

GitHub is a website that hosts Git repositories online.

It lets you:

  • Store your code in the cloud
  • Collaborate with others
  • Contribute to open-source projects
  • Track changes and history

👉 Think of Git as your local notebook and GitHub as Google Drive for code.

🍴 2. What is a Fork?

A fork is a copy of someone else’s repository under your own GitHub account.

Why fork a repo?

  • You don’t have permission to edit the original project
  • You want to experiment safely
  • You want to contribute to open-source projects

How it works:

  1. You click Fork on GitHub
  2. GitHub creates a copy in your account
  3. You can edit freely without affecting the original project

🔄 3. What is a Pull Request (PR)?

A Pull Request is how you suggest changes to someone else’s project.

Think of it like:

“Hey, I made improvements—please review and add them to your project.”

🧠 Simple Flow:

  1. Fork a repo
  2. Clone it to your computer
  3. Make changes
  4. Push changes to your fork
  5. Open a Pull Request

🛠️ 4. Step-by-Step Workflow

Step 1: Fork a repository

On GitHub:

  • Open any repo you want to contribute to
  • Click Fork (top right corner)

Step 2: Clone your fork

“`bash id=”f8c1g2″
git clone https://github.com/your-username/repo-name.git
cd repo-name




---

## Step 3: Create a branch (important!)

Never work directly on main.



```bash id="b9k3m1"
git checkout -b my-feature

Step 4: Make changes

Edit files normally in your code editor.

Step 5: Add and commit changes

“`bash id=”q2r7t9″
git add .
git commit -m “Add new feature or fix bug”




---

## Step 6: Push to your GitHub fork



```bash id="l4x8n3"
git push origin my-feature

Step 7: Open a Pull Request

Go to your fork on GitHub:

  • Click “Compare & pull request”
  • Add a title and description
  • Click “Create Pull Request”

🔍 5. What happens after a Pull Request?

Once you submit a PR:

  • Maintainers review your code
  • They may request changes
  • You can update your PR with more commits
  • If approved → it gets merged 🎉

🔁 6. Keeping your fork updated

Original repos change over time. You need to sync your fork:

Add original repo as upstream:

“`bash id=”u1p9v5″
git remote add upstream https://github.com/original-owner/repo.git




### Pull latest changes:



```bash id="k7s2m8"
git fetch upstream
git merge upstream/main

🧠 7. GitHub Concepts Cheat Sheet

Term Meaning
Repo Project folder on GitHub
Fork Your personal copy of a repo
Clone Download repo to your computer
Branch Separate working version
Commit Saved change
Pull Request Request to merge changes
Merge Combine changes into main project

⚠️ 8. Common Beginner Mistakes

❌ Working on main branch

Always use a feature branch.

❌ Forgetting to fork first

You usually cannot push directly to someone else’s repo.

❌ One huge commit

Break work into small, meaningful commits.

💡 9. Real-world example

Imagine you fix a typo in a website:

  1. Fork repo
  2. Clone it
  3. Fix typo
  4. Push change
  5. Submit PR
  6. Maintainer merges it

🎉 You just contributed to open source!

🧭 Final Thoughts

GitHub is all about collaboration and contribution. Once you understand forks and pull requests, you unlock the ability to:

  • Work on real projects
  • Contribute to open source
  • Build a developer portfolio
  • Work like professionals in tech companies

If you want next, I can show you:

  • 🧪 A practice open-source project to try your first PR
  • 💼 How companies actually use GitHub (real workflow)
  • 🧠 How to write a perfect pull request description
  • ⚡ GitHub Actions (CI/CD automation basics)

Just tell me 👍

API Security in 2026: The Attacks That Are Destroying Production Systems

API Security in 2026: The Real Attacks Destroying Production Systems

Every week, another company announces a data breach. The attackers aren’t using zero-days or sophisticated malware—they’re exploiting the same API vulnerabilities that have existed for years. In 2026, API security is still an afterthought for most teams, and attackers know it.

I spent the last six months analyzing real-world API breaches. Here’s what’s actually hitting production systems.

The OWASP API Top 10 Hasn’t Changed (And Neither Has Industry Response)

The OWASP API Security Top 10 looks almost identical to 2019:

  1. Broken Object Level Authorization (BOLA)
  2. Broken Authentication
  3. Broken Object Property Level Authorization
  4. Unrestricted Resource Consumption
  5. Broken Function Level Authorization
  6. Mass Assignment
  7. Security Misconfiguration
  8. Injection
  9. Improper Inventory Management
  10. Unsafe Consumption of APIs

These aren’t theoretical. Every single one has been exploited in major breaches in the past 18 months.

Attack #1: BOLA — The Silent Data Extractor

Broken Object Level Authorization is responsible for more data breaches than any other vulnerability. The pattern is always the same: an API endpoint exposes an object ID, and the API doesn’t verify if the authenticated user actually owns that object.

Real Example (Simplified from an Actual Breach)

// VULNERABLE API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
  const order = await Order.findById(req.params.orderId);
  res.json(order); // No ownership check!
});

// ATTACK: Iterate through order IDs
// curl https://api.example.com/api/orders/1
// curl https://api.example.com/api/orders/2
// curl https://api.example.com/api/orders/3
// ... extracted 50,000 customer records

How to Fix It

// SECURE API
app.get('/api/orders/:orderId', authMiddleware, async (req, res) => {
  // Explicit ownership check
  const order = await Order.findOne({
    _id: req.params.orderId,
    userId: req.user.id  // Always filter by owner
  });

  if (!order) {
    return res.status(404).json({ 
      error: 'Order not found' 
    });
  }

  res.json(order);
});

The critical lesson: always verify ownership, not just authentication. Authenticated doesn’t mean authorized for that specific object.

Attack #2: Broken Authentication — The JWT Mistakes

JSON Web Tokens are everywhere, and they’re frequently implemented wrong. Here’s a sampling of real JWT vulnerabilities I’ve found in production APIs.

Vulnerability: Algorithm Confusion

// VULNERABLE: Server accepts any algorithm
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['HS256', 'RS256'] // DON'T DO THIS
});

// ATTACK: Change RS256 to HS256 and sign with the public key
// Since the server uses the SAME key for both symmetric and asymmetric,
// the attacker can forge tokens by signing HS256 with the RSA public key
// SECURE: Explicit algorithm allowlist
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'] // Only allow the intended algorithm
});

Vulnerability: None Algorithm

// VULNERABLE: Some libraries accept 'none' algorithm
jwt.verify(token, '', { algorithms: ['none'] }); 
// Produces: {"alg":"none","typ":"JWT"}
// becomes: eyJhbGciOiJub25lIiwidHlwIjoiand0In0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
// SECURE: Explicitly reject 'none'
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'],
  alloweds: { algorithms: ['HS256', 'RS256'] } // Reject none
});

// Or use a library that defaults to rejecting 'none'
const decoded = jwt.verify(token, publicKey);

Attack #3: Unrestricted Resource Consumption — The Denial of Wallet

This one is underrated and increasingly common. Attackers don’t need to crash your service—they just need to make it expensive to run.

Real-World Scenario

# VULNERABLE: No pagination, no limits
@app.get("/api/search")
def search(q: str):
    results = db.query(f"SELECT * FROM products WHERE name LIKE '%{q}%'")
    return results  # Returns ALL matches, attacker controls result set size

# Attack: Search for "a" - returns 2.3M rows
# Your database does a full table scan
# You pay $0.02 per query × 10,000 queries/minute = $200/minute
# SECURE: Strict pagination and query limits
@app.get("/api/search")
def search(
    q: str, 
    limit: int = Query(default=20, ge=1, le=100),  # Max 100
    offset: int = Query(default=0, ge=0, le=10000)  # Max offset
):
    # Parameterized query prevents injection
    results = db.query(
        "SELECT * FROM products WHERE name LIKE %s LIMIT %s OFFSET %s",
        (f"%{q}%", limit, offset)
    )

    # Also add query complexity limits
    if len(q) > 100:
        raise HTTPException(400, "Query too long")

    return results

The Password Reset DoS

# VULNERABLE: Email sending has no rate limit
@app.post("/api/password-reset")
def request_reset(email: str):
    user = db.find_user(email)
    if user:
        send_email(user.email, generate_token())  # No throttle
    return {"message": "If email exists, reset was sent"}

# Attack: Script 1000 emails/second to your SMTP provider
# Cost: $0.10 per email × 86,400,000 emails/day = $8.6M/day
# SECURE: Strict rate limiting per IP and email
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/api/password-reset")
@limiter.limit("3/hour")  # 3 attempts per hour per IP
def request_reset(email: str, request: Request):
    user = db.find_user(email)
    if user:
        send_email(user.email, generate_token())
    # Always return same message to prevent enumeration
    return {"message": "If email exists, reset was sent"}

Attack #4: Mass Assignment — The Hidden Parameter

// VULNERABLE: Trusting client input blindly
app.post('/api/profile', authMiddleware, async (req, res) => {
  await User.updateOne(
    { _id: req.user.id },
    { $set: req.body }  // Client can set ANY field
  );
});

// ATTACK: Send this request
// POST /api/profile
// {"name": "John", "role": "admin", "isVerified": true, "creditLimit": 1000000}
//
// Suddenly the regular user is an admin with unlimited credit
// SECURE: Explicit field allowlist
app.post('/api/profile', authMiddleware, async (req, res) => {
  const allowedFields = ['name', 'bio', 'avatarUrl', 'timezone'];
  const updates = {};

  for (const field of allowedFields) {
    if (field in req.body) {
      updates[field] = req.body[field];
    }
  }

  await User.updateOne(
    { _id: req.user.id },
    { $set: updates }
  );

  res.json({ success: true });
});

Attack #5: The Unprotected Admin Endpoints

# VULNERABLE: Admin endpoint without role check
@app.post("/api/admin/users/delete")
def delete_user(user_id: str):
    db.delete_user(user_id)
    return {"success": True}

# Attack: Anyone who finds this endpoint can delete any user
# No authentication, no authorization check
# SECURE: Explicit role requirement
from functools import wraps

def require_admin(f):
    @wraps(f)
    async def decorated(*args, **kwargs):
        if not request.user or request.user.role != 'admin':
            return {"error": "Forbidden"}, 403
        return await f(*args, **kwargs)
    return decorated

@app.post("/api/admin/users/delete")
@require_admin
@require_auth
def delete_user(user_id: str):
    db.delete_user(user_id)
    return {"success": True}

The Testing Framework You Should Be Using

import httpx
import pytest

class TestAPISecurity:
    """API Security Test Suite - Run these against staging before every deploy"""

    def test_bola_object_level_access(self):
        """Test that users can't access other users' resources"""
        user1_token = self.get_token("[email protected]")
        user2_resource = self.create_resource("[email protected]")

        # User 1 tries to access User 2's resource
        response = httpx.get(
            f"{BASE_URL}/api/resources/{user2_resource.id}",
            headers={"Authorization": f"Bearer {user1_token}"}
        )

        assert response.status_code == 403, "BOLA vulnerability: User can access others' resources!"

    def test_jwt_algorithm_confusion(self):
        """Test JWT algorithm confusion attack"""
        token = self.get_valid_token()
        # Tamper with the algorithm
        header_b64 = base64.b64encode(b'{"alg":"HS256","typ":"JWT"}').decode()
        # Re-sign with a known key
        tampered = f"{header_b64}.{token.split('.')[1]}.{fake_signature}"

        response = httpx.get(
            f"{BASE_URL}/api/protected",
            headers={"Authorization": f"Bearer {tampered}"}
        )

        assert response.status_code == 401, "JWT algorithm confusion succeeded!"

    def test_mass_assignment_protection(self):
        """Test that users can't set admin fields"""
        user_token = self.get_token("[email protected]")

        response = httpx.post(
            f"{BASE_URL}/api/profile",
            headers={"Authorization": f"Bearer {user_token}"},
            json={"name": "Test", "role": "admin", "isVerified": True}
        )

        # Verify admin fields weren't changed
        profile = self.get_profile("[email protected]")
        assert profile["role"] != "admin", "Mass assignment vulnerability!"

    def test_rate_limiting(self):
        """Test that rate limiting prevents abuse"""
        for _ in range(100):
            response = httpx.post(
                f"{BASE_URL}/api/password-reset",
                json={"email": "[email protected]"}
            )

        # 101st request should be rate limited
        response = httpx.post(
            f"{BASE_URL}/api/password-reset",
            json={"email": "[email protected]"}
        )

        assert response.status_code == 429, "Rate limiting not enforced!"

The Security Checklist Before Every Deploy

□ Object-level authorization tested for every endpoint
□ All JWT implementations use explicit algorithm allowlists
□ Rate limiting on all public endpoints
□ Pagination limits on all list endpoints
□ Mass assignment protection via field allowlists
□ Admin endpoints protected by role checks
□ No sensitive data in URL parameters (tokens, IDs)
□ SQL injection protection (parameterized queries)
□ No stack traces or internal errors in responses
□ CORS properly configured
□ Security headers present (CSP, X-Frame-Options, etc.)

The Harsh Reality

API security in 2026 is still years behind application security. Most teams have:

  • No API security testing in CI/CD
  • No API inventory (how many endpoints do you have? Do you even know?)
  • No rate limiting on 80% of endpoints
  • Authentication without authorization checks

The attackers know this. They’re scanning for these vulnerabilities at scale, automated, 24/7.

The good news: fixing these isn’t hard. It just requires making security testing a first-class citizen in your development process.

What’s your biggest API security challenge? Found any interesting vulnerabilities in the wild? Let’s discuss.

Further reading: OWASP API Security Top 10 — still the best starting point for API security fundamentals.

Building a Real-Time Collaborative Task Board: How Gemini Helped Me Build for the Google I/O Challenge 2026 🚀

🚀 Google I/O Challenge 2026: Collaborative Task Board

Managing workflows in modern remote-first teams demands highly fluid, non-blocking digital hubs. For the Google I/O Challenge 2026, I wanted to design and build a Collaborative Task Board that replicates the rich interactive experiences of industry-standard visual workspaces—built in record time under the guidance of Google Gemini in AI Studio.

🔗 Try the Live App Here: Collaborative Task Board Live Link

(Tip: Open the URL in two separate windows or share it with a teammate to watch cursors sync live!)

🎨 Creative Vision & Application Features

The Collaborative Task Board is designed on top of a comfortable, premium, high-contrast Slate Indigo theme utilizing precise typography pairs: Space Grotesk display headings alongside JetBrains Mono for live status elements.

Key Capabilities Included:

  • 👥 Real-Time User Presence: Teammates onboard instantly with customized nicknames and color identity themes, forming an active avatar stack representing who is inside the room.
  • 🖱️ Adaptive Cursor Trails: Witness low-latency mouse tracking broadcasts complete with custom colors and labels matching each coworker’s cursor.
  • 🔒 Intelligent Focus Locks (Anti-Collision): If a teammate is editing cards, configuring properties, or appending discussions, the card is locked system-wide with a glowing visual banner to prevent editing collisions.
  • 📦 Express-WebSocket State Machine: High-performance socket pipelines sync all structural change actions—creation, descriptions, priority shifts, comment additions, and drag motions—globally.
  • 💬 Integrated Chat Forums: Each room coordinates an isolated workspace chat to talk in real-time alongside Kanban columns.
  • 🔑 Dynamic Room Routing: Simply change the ?room= link query or inputs to create isolated spaces for separate teams.

🤖 Co-Architecting with Gemini on AI Studio

Building a truly real-time interactive coordinate tracking space is notoriously tricky to scale and refine. Gemini acted as a world-class senior developer, helper, and designer at every stage:

  1. Architecture Design: Gemini drafted the structured type interfaces and strict client/server communication state schemas, ensuring that backend events maps to the unified source of truth.
  2. WebSocket Integration: Gemini successfully avoided the classic React WebSocket footprint trap (re-instantiating connections on state changes) by leveraging persistent useRef handlers, combining robust fallback backoff reconnection logic.
  3. Performance Optimization: To keep cursor broadcast packets lightweight, Gemini suggested a 45ms throttle on pointer updates, reducing websocket server load by up to 80% while maintaining premium human-eye fluidity.
  4. No Mock Infrastructure: Guided by AI Studio’s architectural guidelines, Gemini crafted a real Node/Express backend on disk instead of simple mockup timeouts, giving us live server and client synchronizations dynamically served on Cloud Run.

🛠️ Tech Stack & Implementation Details

  • Frontend: React 19, Tailwind CSS, Lucide icons, and motion (Framer) for fluid task rearrangements.
  • Backend: Node.js/TypeScript running an Express server bound with robust memory states, hosted on Google’s scalable Cloud Run container environment.
  • Persistence & Sync: Raw ws WebSocket servers combined with robust JSON snapshots saved automatically to local data volumes matching active operations.

💭 Reflection & Google I/O Appreciation

Developing this project using AI Studio Build has been an incredible experience. The ability to express intent naturally and immediately receive clean TypeScript modules, matching proper bundler builds and actual socket integrations, makes it incredibly fun to take products from simple sketches to live, ready-to-test systems.

Let me know what you think by jumping into my collaborative workspace room! Let’s build together. 🤖

Built with ♥ for the Google I/O Challenge 2026.

Writerdeck con Debian tty: convertir un laptop viejo en máquina de escribir

Hay un patrón que empieza a repetirse en blogs, foros y cuentas de Mastodon: gente que toma un laptop viejo, le borra el sistema operativo gráfico completo y lo convierte en una máquina dedicada exclusivamente a escribir. Le llaman writerdeck, y el ejemplo más reciente lo publicó Verónica Explains en su blog personal con una receta que cabe en menos de cinco comandos.

El planteo es simple: si el problema es la dispersión que producen las pestañas del navegador, las notificaciones y el cambio constante de contexto, la solución no es disciplina ni una app de bloqueo, sino quitarle al hardware la capacidad misma de distraer. Y para eso, un Debian Trixie en modo consola sobra.

TL;DR

  • Verónica Explains publicó “It’s time to talk about my writerdeck” documentando su setup minimalista para escritura concentrada.- La base es Debian Trixie en modo consola: sin X11, sin Wayland, sin entorno de escritorio ni navegadores.- El hardware es un System76 Galago Pro de seis años con teclado mecánico y pantalla mate ideal para sesiones largas.- Cinco piezas clave del stack: network-manager, kmscon, tmux, neovim y syncthing, todas instalables con apt en minutos.- kmscon habilita tipografías personalizadas y más de 16 colores en la tty, además de zoom con ctrl-+ como un navegador.- El movimiento writerdeck crece en Reddit, Hacker News y Mastodon como reacción al multitasking forzado de los SO modernos.- Syncthing reemplaza Dropbox o iCloud sincronizando archivos vía LAN sin servidor central ni cuenta de terceros.

Qué es exactamente un writerdeck

Un writerdeck es un dispositivo dedicado a escribir y solo a escribir. La idea no es nueva: los procesadores de palabras de los 80 —los Tandy WP-2, las máquinas Brother, los AlphaSmart que todavía aparecen en eBay— ya eran exactamente eso. Lo que sí es nuevo es la versión moderna del concepto, donde en lugar de comprar hardware específico se reutiliza un laptop existente y se le quita todo el software que pueda interrumpir el flujo.

El término se popularizó en r/writerdeck en Reddit, donde cientos de personas comparten builds: desde Raspberry Pi con teclados mecánicos hasta computadoras de hace una década corriendo sistemas operativos de los 90. La constante en todos los builds es la misma: nada de navegador, nada de notificaciones, una sola tarea posible en pantalla.
El stack mínimo: Debian, kmscon, tmux y neovim sobre la consola Linux.

La receta concreta del writerdeck Debian sin escritorio

El paso a paso que documenta Verónica empieza por el instalador de Debian en modo texto. La diferencia con una instalación normal está en una pantalla muy específica: la de Software selection, donde por defecto vienen marcados “Debian desktop environment” y “GNOME”. El truco es desmarcar ambos.

Lo que queda al terminar es una máquina que arranca a un login de consola negro sobre blanco, sin ningún programa gráfico instalado. Para alguien acostumbrado a un escritorio moderno, la primera impresión es deliberadamente fea. Para el writerdeck, ese es exactamente el punto.

💡 Tip: si venís de Ubuntu o Mint y nunca instalaste Debian directamente, hay una trampa: para tener sudo funcional debés dejar la contraseña de root en blanco durante la instalación. Esto le indica al instalador que querés usar el modelo sudo en lugar del modelo root tradicional de Debian.

Las cinco piezas clave del stack

network-manager para conectarse sin editar configs

Lo primero que instala Verónica después de logearse es network-manager, no por la conectividad en sí, sino por la herramienta TUI que trae:

sudo apt install network-manager
sudo nmtui

La interfaz de nmtui es exactamente eso que esperamos de una TUI bien hecha: un menú navegable con flechas, una pantalla para escanear Wi-Fi disponibles, una para conectar. Sin editar /etc/network/interfaces ni pelearse con wpa_supplicant. La filosofía es: el writerdeck va a estar offline el 95% del tiempo, pero cuando necesite conectarse para sincronizar texto, debe ser un proceso de diez segundos.

kmscon: la pieza que cambia todo

La tty estándar de Linux tiene dos limitaciones que la hacen molesta para escribir durante horas: solo soporta 16 colores y no permite cambiar la tipografía a algo legible más allá del bitmap que viene por defecto. kmscon resuelve ambas cosas.

En Debian Trixie está disponible en backports, así que el proceso de instalación pasa por agregar el repositorio:

# Agregar a /etc/apt/sources.list:
deb http://deb.debian.org/debian/ trixie-backports main contrib non-free non-free-firmware

# Luego:
sudo apt update
sudo apt install -t trixie-backports kmscon

Tras un reboot, la tty deja de ser ese rectángulo monocromático de los 90 y se convierte en una consola que acepta tipografías TrueType, soporta zoom con Ctrl++ y Ctrl+-, y maneja paletas de 256 colores. Para quien va a pasar tres horas seguidas escribiendo, esto es la diferencia entre fatiga visual y comodidad real.

tmux para panel de estado y multiplexor

El siguiente nivel es tmux. Aunque la tty soporta múltiples sesiones con Ctrl+Alt+F1..F6, tener un multiplexor encima permite paneles dentro de la misma sesión y, sobre todo, una barra de estado configurable en la parte inferior de la pantalla.

La barra de estado del writerdeck de Verónica muestra batería (vía acpi), brillo de pantalla (vía light) y la hora. Información mínima pero suficiente: no hay popups, no hay notificaciones, solo una línea fija que se actualiza cada minuto.

sudo apt install tmux acpi light

neovim como editor

La elección de neovim sobre vim es preferencia personal —cualquier editor terminal funciona— pero Verónica lo justifica como una excusa para aprender la versión moderna del editor. La combinación con el plugin vim-vimwiki le da algo importante: un wiki personal con enlaces internos navegables con Enter, todo en archivos de texto plano y sin depender de servicios externos.

syncthing para backup descentralizado

El problema obvio de un dispositivo dedicado a escribir es: ¿qué pasa si se rompe? La respuesta es syncthing, que sincroniza la carpeta de trabajo con otros dispositivos (laptop principal, NAS, teléfono) usando descubrimiento por LAN y cifrado punto a punto, sin pasar por servidores de terceros y sin cuentas que mantener.
El writerdeck pensado para escribir afuera: pantalla mate, batería larga, cero distracciones.

Por qué este patrón crece en 2026

El movimiento writerdeck no es solo nostalgia. Coincide con una conversación más amplia sobre cómo el software moderno secuestra la atención. El subreddit r/writerdeck pasó de unos 3.000 a más de 25.000 suscriptores en los últimos dos años. En Mastodon, el hashtag #writerdeck aparece varias veces por semana con builds nuevos. Hacker News discute regularmente posts sobre Pomera, AlphaSmart o builds caseros, y casi siempre llegan al top 10 del día.

Tres factores explican el crecimiento. Primero, la fatiga de la integración total: cada app moderna quiere notificarte algo, y las soluciones tradicionales —modo No molestar, apps de bloqueo— funcionan mientras tengamos voluntad para activarlas. Segundo, el costo de los IDEs y editores que cargan IA por defecto: para escribir prosa no se necesita autocompletado de modelos de 70B parámetros, y la sugerencia constante interrumpe el ritmo. Tercero, el redescubrimiento del hardware viejo como suficiente: un laptop de 2019 sigue siendo más potente que cualquier procesador de palabras dedicado de 1995, y reciclarlo cuesta cero.

Hay también una lectura más profunda: el writerdeck es un rechazo silencioso al supuesto de que la productividad mejora con más herramientas. Verónica menciona en el post que tiene un problema de atención y que la solución fue quitarle al hardware la posibilidad misma de ofrecerle alternativas. Es el equivalente digital de salir a escribir a un café sin Wi-Fi, pero permanente y sin depender de la conexión que tenga el local del barrio.

Cómo replicar el writerdeck en Windows, macOS o Linux

Si querés probar el concepto sin reinstalar tu sistema operativo, hay caminos parciales:

En Linux (cualquier distro), podés usar la receta de Verónica casi tal cual. Cambia el gestor de paquetes según tu distro:

# Debian/Ubuntu/Mint
sudo apt install kmscon tmux neovim syncthing

# Fedora
sudo dnf install kmscon tmux neovim syncthing

# Arch
sudo pacman -S kmscon tmux neovim syncthing

En macOS, no hay equivalente a kmscon porque el sistema no expone una tty real accesible al usuario, pero podés simular el experimento con iTerm2 en modo pantalla completa, una sola pestaña, neovim, y bloqueando el navegador con apps como Cold Turkey o Self Control:

brew install neovim tmux syncthing

En Windows, la opción más limpia es WSL2 con Debian o Ubuntu, corriendo Windows Terminal en pantalla completa:

wsl --install -d Debian

# Dentro de Debian/WSL:
sudo apt install neovim tmux syncthing

El experimento puro requiere tty real, pero la versión soft en cualquier OS captura la mayor parte del beneficio: una sola ventana, un solo editor, sin navegador a un Cmd+Tab de distancia.

Diagrama del flujo del writerdeck

Para visualizar cómo se conectan las piezas del stack:

graph LR
A["Hardware: laptop viejo"] --> B["Debian tty"]
B --> C["kmscon: fonts y colores"]
B --> D["network-manager: Wi-Fi"]
C --> E["tmux: barra de estado"]
E --> F["neovim: editor"]
F --> G["archivos .md/.txt"]
G --> H["syncthing: backup LAN"]
H --> I["Laptop principal o NAS"]

Impacto y análisis

Lo interesante del writerdeck no es la pila técnica —cualquier admin de Linux la replica en una tarde— sino lo que dice del estado actual del software. Necesitamos quitarle capas a la computadora moderna para poder enfocarnos en una sola tarea. La promesa del SO multitarea, que en los 90 era liberadora, en 2026 se siente como una carga que cargamos por inercia.

También vale notar que la receta funciona porque Linux todavía permite este nivel de minimalismo. En macOS o Windows no podés realmente quitar el navegador del sistema —Safari y Edge están integrados al SO de formas no documentadas y no soportadas para removerlas. El concepto writerdeck es, en cierta forma, un argumento implícito a favor del software libre: solo en un sistema donde podés borrar lo que no necesitás es posible construir un dispositivo verdaderamente dedicado a una sola función.

📌 Nota: Pomera DM250, el writerdeck dedicado de King Jim más popular, cuesta cerca de 600 USD importado de Japón. Un laptop de seis años convertido con Debian cuesta cero pesos adicionales y tiene mejor teclado.

Qué sigue

La comunidad alrededor del concepto ya empezó a producir variantes interesantes: writerdecks basados en Raspberry Pi 5 con pantallas e-ink que duran semanas con una carga, builds con teclados mecánicos personalizados conectados a tablets viejas, y proyectos open source en GitHub que empaquetan toda la receta en una imagen ISO lista para grabar.

El siguiente paso lógico, según comentan varios usuarios en los hilos de Hacker News sobre el post original, es estandarizar una imagen Debian preconfigurada que incluya todos los paquetes mencionados, una configuración decente de tmux y un esquema de colores para neovim pensado específicamente para prosa larga. Hasta entonces, la receta de Verónica es la documentación más completa y reproducible disponible.

📖 Resumen en Telegram: Ver resumen

Preguntas frecuentes

¿Para qué sirve un writerdeck si ya tengo un laptop normal?

Para eliminar físicamente la posibilidad de distraerte. Aunque podés ser disciplinado en un laptop normal, un dispositivo dedicado sin navegador remueve la tentación por completo. Es la diferencia entre tener una bolsa de papas fritas en la mesa y no comprarlas.

¿Por qué Debian y no Ubuntu o Fedora?

Cualquier distro funciona. Verónica eligió Debian Trixie por estabilidad y porque su instalador permite explícitamente no instalar entorno gráfico desde el primer momento. En Ubuntu hay que usar la variante Server para lograr lo mismo, y en Fedora se necesita la opción Minimal Install.

¿Puedo usar Windows o macOS para esto?

No del todo, porque no podés quitar el navegador del sistema. La versión más cercana en macOS es iTerm2 a pantalla completa con neovim, y en Windows es WSL2 con Windows Terminal. Ambas funcionan pero el OS sigue siendo distractible por naturaleza.

¿Qué pasa si necesito buscar algo en internet mientras escribo?

El punto del writerdeck es que esa interrupción cuesta. Algunos usuarios mantienen una sesión de w3m o links2 (navegadores en consola) bloqueados detrás de un alias difícil. Otros simplemente anotan la pregunta en el texto con un marcador como [FACT-CHECK] y la resuelven más tarde en el laptop principal.

¿Cuánto tarda configurarlo desde cero?

La instalación de Debian toma 20 a 30 minutos. Instalar y configurar los paquetes adicionales (kmscon, tmux, neovim, syncthing) suma otros 30 a 45 minutos. En menos de dos horas tenés el writerdeck funcionando y listo para escribir.

¿Sirve para programar también?

Sí, sin problema. neovim con plugins de LSP puede ser un IDE completo. Lo que cambia es la cultura: si lo usás para programar perdés la concentración pura del concepto. La mayoría de gente que arma un writerdeck lo dedica solo a prosa.

Referencias

  • veronicaexplains.net — Post original “It’s time to talk about my writerdeck” de Verónica Explains.- syncthing.net — Sitio oficial de Syncthing, sincronización descentralizada P2P.- neovim.io — Página oficial de neovim, el editor que usa el setup.- github.com/tmux/tmux — Repositorio oficial de tmux, multiplexor de terminales.- debian.org — Documentación oficial de Debian, la distribución usada en el writerdeck.

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

How does an AI agent pick from 686 skills in a second?

I ran an empirical test on the “skills as semantic router” pattern for Claude Code agents. I indexed 686 randomly sampled skills from a 4,556-skill community corpus into mesh-memory, embedded them with a single sentence-transformer model, and ran a fixed set of eight task queries through it. Here are the headline numbers: strict top-1 accuracy 62.5%, top-5 cluster accuracy 87.5%, sub-second query latency, ~500 tokens loaded per task versus the ~228K tokens just to keep names + descriptions of all 4,556 skills in the system prompt (the default behavior, even with Anthropic’s progressive disclosure). That is roughly a 456x context-window saving with the right skill landing in the agent’s top-5 candidates seven times out of eight.

This post explains why I ran the test, how it was set up, what the results actually show, and where the pattern breaks honestly. The full source for the runner and queries is reproducible.

Why progressive disclosure is not enough at scale

Anthropic’s Claude Code skills (and Cursor’s equivalents, and every other agent framework’s skills) ship as markdown files in a folder. Each one has a name and a short description in its frontmatter. The default loading strategy is what Anthropic calls “progressive disclosure”: the agent reads every skill’s name + description into its system prompt at startup, and only loads the full body when it decides to invoke one.

Progressive disclosure handles the body problem — you do not pay for skill bodies you never use. But it does not handle the index problem. Even just the names and descriptions are loaded for every skill, every session, before any work starts. At fifty skills you spend roughly 2.5K tokens on the catalog. At 200 skills the catalog eats 5% of a Claude Sonnet 200K context window before you have asked anything. The math gets ugly fast:

Skills in catalog Tokens for names + descriptions Share of 200K context
100 ~5K 2.5%
500 ~25K 12.5%
1,000 ~50K 25%
2,000 ~100K 50%
4,000 ~200K will not fit
4,556 (full community corpus) ~228K overflow

Even when the catalog physically fits, attention quality on a long list of similar items degrades. Past about 1,000 entries the agent starts making wrong picks on hand-eye-distinguishable cases. There is also no garbage collection: nobody removes stale skills, nobody flags duplicates, the catalog only grows.

The semantic router pattern decouples the catalog from the prompt. Each skill’s name and description is stored once in an embedding index, with a tag pointing to the SKILL.md file on disk. At task time the agent runs a single semantic-search call against the task description, gets the top-5 candidates, picks one, and reads the full body only for the one it picked. Token cost per turn is constant regardless of catalog size.

That is the theory. The question is whether the search actually returns relevant skills in real conditions.

The test setup

Corpus. antigravity-awesome-skills, a public collection of Anthropic-format skills from the community. 4,556 SKILL.md files, deduplicated by directory. Each file has a YAML frontmatter (name, description, tags) and a markdown body.

Sample. 1,000 skills selected by random.shuffle(seed=42) from the sorted file list. Of those, roughly 200 were silently rejected by the mesh bulk-ingest endpoint (likely a content-validation filter), 25 failed in transit on a single wave, and 86 ended up stuck in the “pending” state after embedding — a known mesh-memory worker stall. Final indexed corpus: 686 skills.

Routing document. For each skill, the embedded text is exactly name + "nn" + description. The full SKILL.md body stays on disk; mesh holds only the routing signal plus a skill_path tag with the disk path. That is roughly 50-200 tokens per skill in the index.

Embedding model. intfloat/multilingual-e5-base, running locally via sentence-transformers, 768-dimensional vectors, stored in Postgres + pgvector. Ten parallel embedding workers, throughput ~38 docs/minute on a single CPU container.

Queries. Eight diverse task descriptions, written before looking at the corpus, designed to span common dev work:

  1. “deploy docker production”
  2. “analyze stock market data”
  3. “write marketing email”
  4. “optimize slow SQL query”
  5. “security audit web app”
  6. “set up CI CD pipeline python”
  7. “debug memory leak C++”
  8. “build React TypeScript component”

For each query I asked mesh for the top-5 most similar skills and inspected the names plus cosine similarity scores.

Metrics.

  • Strict top-1: the first result is the kind of skill a human reviewer would pick for the task without reservation.
  • Loose top-1: the first result is in the right family but not a perfect match (e.g. an Azure deploy skill for a Docker deploy query).
  • Top-5 cluster: at least one of the first five results is a strong match the agent could reasonably read and use.

The results

After all 686 skills were indexed, query-by-query:

Query Top-1 result (sim) Cluster verdict Strict
deploy docker production azd-deployment (0.86) 3 of 5 are deploy skills (azd, appdeploy, vercel) loose
analyze stock market data xvary-stock-research (0.87) + alpha-vantage at #4 YES
write marketing email copywriting (0.86) blog-writing, writer in cluster YES
optimize slow SQL query food-database-query (0.85) spark-optimization #4, no real SQL skill NO
security audit web app laravel-security-audit (0.88) aws-security, burp-suite, web-security-testing — 4 of 5 YES
set up CI CD pipeline python gitlab-ci-patterns (0.87) circleci-automation #2 loose
debug memory leak C++ c-pro (0.86) gdb-cli, debugger, systematic-debugging YES
build React TypeScript component react-flow-node-ts (0.88) 5 of 5 frontend-relevant YES

Strict top-1: 5 of 8 = 62.5%.
Top-5 cluster: 7 of 8 = 87.5%.
Cosine similarity range for top-1: 0.83-0.88.
Query latency: under 1 second across all runs.

The convergence curve

The corpus was built in waves of 100 skills, with the full query suite re-run after each wave. That gives a sense of how router quality scales with corpus depth:

Wave Indexed Strict top-1 Top-5 cluster Notable arrivals
0 91 25% (2/8) ~70% debugger, javascript-typescript-scaffold
1 177 43% (3/7*) ~85% web-security-testing, alpha-vantage, performance-optimizer
5 500 ~50% (4/7*) ~85% copywriting, laravel-security-audit, gitlab-ci-patterns, c-pro, react-flow-node-ts
9 686 62.5% (5/8) 87.5% xvary-stock-research

*one query timeout on the run

Two observations land hard.

Top-5 cluster saturates early. By 500 indexed skills (about 11% of the full corpus) the cluster metric was already at 85% and barely moved with the next 186. For most queries the relevant family of skills was already in the index; what changed later was which member led the cluster.

Strict top-1 keeps climbing. Going from 500 to 686 indexed skills bumped top-1 from 50% to 62.5%. The improvement came from one specific skill (xvary-stock-research) finally landing in the sampled portion. Each new wave was a chance for an exact-match skill to surface for one of the eight queries.

When the router fails honestly

The SQL query never produced a real top-1. The top-1 was food-database-query (a false match on the word “query”), and the cluster contained spark-optimization and cqrs-implementation but no actual SQL-tuning skill. Looking at the unindexed portion of the corpus, sql-optimization-patterns exists — it just landed in waves 10-45 of the shuffle, beyond our 1,000-sample window.

This is the honest face of the pattern. Router accuracy is bounded by corpus depth, not by the search algorithm. Embeddings did their job perfectly: every query returned coherent clusters with similarities in the 0.83-0.88 range. When the right skill was in the index, the router found it. When it was not, no search quality compensates.

The practical consequence: this pattern wins as your skill collection grows. With under 30 skills there is no point — eager-load them and move on. Past 100, the math starts mattering. Past 1,000, it is the only sustainable option.

The token math

Per task turn:

  • Default loading (names + descriptions for 4,556 skills): ~228,000 tokens in the system prompt. Will not fit in a 200K context window. In a 1M context window it consumes 23% of the budget on catalog alone. Progressive disclosure saves the bodies, not the index.
  • Router: 1 search request (negligible tokens), top-5 results returned (~500 tokens), 1 full SKILL.md read for the picked skill (~500-1,500 tokens depending on size). Total per turn: under 2K tokens.

That is a 100x to 450x reduction depending on which skill ends up being read. The kicker: the router cost is constant whether the catalog holds 100 skills or 100,000.

Open issues to be honest about

Stuck-on-embed leak. Of 772 documents that made it into the mesh database, 86 (11.1%) ended up stuck in the “pending” state and never got an embedding. The mesh-memory worker code has MAX_CONSECUTIVE_ERRORS = 10, which halts a worker on a bad document and silently leaves the rest of the queue. Worth filing as an upstream issue; in the meantime, restarting the worker on a fresh container drains the backlog.

Silent bulk-ingest drops. About 200 of 1,000 documents sent never appeared in the database at all — the bulk PUT returned success but mesh stored fewer rows than were posted. Likely a content-validation filter on empty or near-empty documents. Worth investigating but did not affect the search-quality results.

The 10-worker bump. Default NUM_WORKERS = 3 is bottlenecking on a single-CPU container. Bumping to 10 quadrupled throughput (~9 в†’ ~38 docs/min) with no observed degradation. This change should be parameterizable in the next mesh release.

Query timeouts. One query in the wave-5 run timed out at the default 30s; rerunning succeeded. Likely a cold-cache warmup issue, not a search-correctness problem.

Reproduce it yourself

Everything used in this test is open source. The runner is roughly 70 lines of Python:

  • Corpus: antigravity-awesome-skills
  • Memory store: mesh-memory (MIT-licensed, ships with an MCP server for Claude Code and Cursor)
  • Runner + queries + raw results: posted alongside this article

Drop the corpus into mesh, run a few queries against it, and look at what you get back. If the pattern fits your workflow, the wiring is trivial: a single MCP call in your claude.md that says “before working on any task, search mesh for type:skill matching the task description, then load the top result.”

Related

If you have not read it yet, the companion post How to Organize Your Claude Skills Without Drowning in Files covers the versioning side of this same pattern — storing skills as memory documents with date tags, so the latest version is one query away and old versions never need to be deleted. Together the two posts cover both “how do you store skills sanely” and “how do you retrieve them sanely.”

Originally published on klymentiev.com

The Treasure Hunt Engine Mistake That Brings Down Most Hytale Servers

The Problem We Were Actually Solving

In the midst of our server performance woes, I began digging into the Treasure Hunt Engine’s source code to pinpoint the bottleneck. I set up a profiling tool to monitor its memory allocation patterns and realized that the engine spent an inordinate amount of time waiting on the Java Virtual Machine’s (JVM) garbage collector to free up memory. This was during a burst of concurrent player requests for treasure locations, when our server’s heap memory limit was consistently breached. Every millisecond wasted on garbage collection was directly costing us player engagement and server stability. It was clear that our JVM and its allocation pattern were the actual bottlenecks we needed to address.

What We Tried First (And Why It Failed)

Initially, I thought that by simply tweaking the JVM’s garbage collection configuration and increasing the heap size, we could work around the issue. I experimented with different algorithms like Concurrent Mark-and-Sweep (CMS) and G1 to see if they’d mitigate the pauses. However, these attempts ultimately proved ineffective, and server crashes persisted. Looking back, I realize that trying to optimize garbage collection without addressing the underlying memory safety issues was a delaying tactic rather than a solution. It masked the symptoms but didn’t change the root cause.

The Architecture Decision

After revisiting our stack and design, we made a critical decision: we would rewrite the Treasure Hunt Engine in Rust. I know what you’re thinking – rewriting a component in a different language can be a daunting task, especially when coupled with the learning curve of Rust itself. But the numbers told a different story: by using Rust’s ownership and borrowing system, we could ensure memory safety without GC pauses and ensure that our allocations were deterministic and predictably efficient. Specifically, with Rust’s stack-based allocation, we were able to reduce allocation counts by 85% for the same use case, and the engine’s latency plummeted by a whopping 65% across all user requests. That’s the difference between a stable server and one that crashes under load.

What The Numbers Said After

Here’s a snapshot of our profiler output before and after the rewrite:

  • Before:
    • Mean allocation count: 2.5K
    • Mean allocation size: 128 bytes
    • Mean GC pause duration: 150 ms
  • After:
    • Mean allocation count: 385
    • Mean allocation size: 32 bytes
    • Mean GC pause duration: 0.5 ms
      The changes were tangible, and the engineering complexity was manageable due to Rust’s safety-centric design. It also allowed us to experiment with other performance optimizations with confidence, like reducing the engine’s computation intensity and leveraging async programming to offload computationally expensive tasks.

What I Would Do Differently

In hindsight, I would have bitten the bullet and rearchitected the engine in Rust sooner, rather than relying on workarounds. The engineering learning curve, while steep, was compensated by the long-term stability and predictability we’ve gained. If you’re a fellow production operator struggling with poorly optimized Treasure Hunt Engines, I urge you to consider rewriting your component in a language designed for performance, safety, and predictability – like Rust.

Speed Over Size: Why Gemini 3.5 Flash is the Most Important Update at Google I/O 2026

This is a submission for the Google I/O Writing Challenge

Introduction

Google I/O 2026 just wrapped up, and as expected, the announcements were packed with futuristic ideas—Android XR smart glasses, cinematic video generation, and other flashy demos that usually dominate the headlines.

But the update that actually stood out to me wasn’t the most visually impressive. It was something much more practical: Gemini 3.5 Flash.

As someone who spends a lot of time learning, coding, and building projects, I’ve used enough AI assistants to notice a pattern. Today’s models are incredibly powerful—but they’re also heavy. And that weight shows up in one very specific way: latency.

Whether you’re debugging code, generating scripts, or trying to understand a terminal error, there’s always that pause. The response slowly streams in token by token, and even a few seconds of delay can break your rhythm.

Gemini 3.5 Flash feels like Google’s answer to that problem.

What is Gemini 3.5 Flash?

Google introduced Gemini 3.5 Flash as the new default model powering parts of the Gemini ecosystem. It’s designed specifically for speed—especially for real-time, multi-step tasks where responsiveness matters.

According to Google, it runs up to 4× faster than other frontier models.

But speed alone isn’t the interesting part.

Normally, when a model gets faster, you expect it to lose some depth or accuracy. What makes Gemini 3.5 Flash stand out is that it doesn’t seem to follow that trade-off. Google claims it actually beats older “Pro” models on advanced tasks, including coding and autonomous workflows, with a reported 76.2% on Terminal-Bench 2.1.

So instead of being a “lite” version, it feels more like a model that was engineered from the ground up to reduce delay without sacrificing capability.

My Perspective: Protecting the Flow State

People often debate which model performs best on benchmarks, but in real-world development, something else matters more: momentum.

When you’re deep into debugging something—maybe a broken VM, messy logs, or a database query that refuses to behave—you’re thinking fast. Your brain is building a chain of logic step by step.

In those moments, AI is most useful when it behaves like an extension of your thoughts. You ask a question, you get an answer, and you keep moving.

But when there’s a 10–15 second delay, something breaks. You tab away. You lose focus. Sometimes you don’t even come back to the problem with the same clarity you had a moment before.

That’s why Gemini 3.5 Flash is interesting. If it really delivers consistent frontier-level reasoning at high speed, it doesn’t just make AI “better”—it makes it feel invisible. Like part of your development environment instead of a separate tool you wait on.

Final Thoughts & Critique

I’m genuinely excited to see Gemini 3.5 Flash roll out across Google AI Studio and developer tools. It feels like a very deliberate shift toward optimizing for real developer experience instead of just benchmark leadership.

That said, I still have some skepticism.

Speed always comes with a question: what’s the compromise? A faster model can sometimes feel more confident than correct, especially when handling complex, multi-file reasoning or long context chains. That’s where things like hallucinations or shallow analysis could show up.

The real test won’t be keynote demos—it will be how it performs in messy, real-world codebases where nothing is clean or predictable.

Still, if it lives up to the promise, Google has focused on something that actually matters to developers more than anything else: removing friction.

And honestly, that’s something worth paying attention to.