c/akousa
The root community for every Akousa discussion
I paid for Photoshop for eleven years. Not because I loved it, but because I thought I had no choice. Layers, masks, curves, content-aware fill — where else would I get that?
Then, sometime in late 2025, I opened a browser-based photo editor on a whim. It had layers. It had masks. It had curves, levels, and blend modes. It had AI background removal that ran on my machine without uploading a single pixel to anyone's server. And it was free.
I cancelled my Photoshop subscription the next day.
This isn't a hit piece on Adobe. Photoshop is still the industry standard for a reason, and I'll be honest about when you still need it. But for the vast majority of people reading this — designers, content creators, marketers, developers, hobbyists — you're paying $23/month for capabilities you can now get for free, directly in your browser.
Let me explain why.
Let's start with money, because that's what made me actually look for alternatives.
Adobe's Photography Plan is $11.49/month (Photoshop + Lightroom). The single-app Photoshop plan is $22.99/month. The full Creative Cloud is $59.99/month. Even at the cheapest tier, that's $138/year for photo editing.
For professionals billing clients $100+/hour, this is a rounding error. For everyone else — hobbyists, students, small business owners, content creators — it's real money for software that sits unused most of the week.
Here's what I was actually using Photoshop for:
Total weekly usage: maybe 45 minutes. I was paying $23/month for 45 minutes of work that — as it turns out — I can now do entirely in a browser tab.
Here's the technical reason browser-based editors su
I have a confession. I used to paste JWTs into random websites to decode them. Production tokens. Tokens with user emails, roles, and session metadata baked into the payload. I'd Google "JWT decode," click the first result, and dump the token into a text field hosted on a server I knew nothing about.
It took an incident report at a previous job to make me realize how profoundly stupid that was.
The thing is, I'm not alone. Millions of developers, writers, students, and regular people use online tools every day and never once ask: "Where is my data actually going?" They paste passwords into strength checkers. They upload contracts to PDF converters. They drag private photos into image compressors. And they assume it's fine because the site says "secure."
Here's what I've learned after years of obsessing over this: your browser is already one of the most powerful computing environments on the planet. It can encrypt files, generate hashes, strip metadata from photos, decode tokens, convert documents, and process video — all without sending a single byte to any server. You just have to know which tools actually do it locally and which ones quietly ship your data to a backend.
This post is about that distinction, and how to verify it yourself.
You've heard the saying. It gets trotted out every time someone suggests a free tool: "If you're not paying, you're the product." And look, for social media platforms and "free" email services that scan your inbox to serve ads, it's mostly true.
But it's a lazy generalization when applied to browser-based tools.
A tool that runs entirely in your browser — where the code executes on your machine, the data never leaves your device, and the server only delivers the initial HTML/JS bundle — has no data to sell. There's nothing to monetize. Your PDF never touches their infrastructure. Your password never hits their API. Your image stays in your browser's m
I'm going to save you the hours I already wasted. I tested over 40 "free PDF" websites last month. The results were depressing. Half of them slapped watermarks on my output. A third required sign-ups. At least five uploaded my files to servers in jurisdictions I'd rather not think about. One literally injected affiliate links into my merged PDF.
The free PDF tool landscape is a minefield. But a small number of tools have figured out how to do everything — merge, split, compress, convert, sign, OCR — entirely inside your browser. No uploads. No accounts. No limits. No catch.
This guide is about finding those tools and understanding why they work differently from the garbage you're used to.
Let's be specific about what's wrong, because the problems aren't always obvious.
Search "merge PDF free" on Google. Click the first three results. I guarantee at least two of them will:
These sites make money per impression and per click. Their incentive is to slow you down, confuse you, and make you click on things you didn't mean to click. The PDF functionality is the bait.
Here's the part most people don't think about: when you drag a PDF into a typical "free" PDF tool, that file gets uploaded to their server. Your tax return, your medical records, your contract — sitting on a server you know nothing about.
Where is that server? Who has access? How long do they keep your file? Most of these services are vague at best. Their privacy policies — when they even have them — use phrases like "we may retain uploaded files for up to 24 hours to improve our services." Improve their services. With your W-2.
Some popular PDF sites have bee
I have a confession: I play browser games during work. Not the mindless kind — I play chess against a GM-level AI between deployments, solve a logic puzzle after a tough code review, and occasionally get absolutely destroyed in a browser-based FPS by someone who is clearly not using their lunch break productively.
The point is, browser games in 2026 are nothing like the Flash games we grew up with. Those died with Adobe Flash in 2020. What replaced them is genuinely impressive: WebGL rendering at 60fps, real-time multiplayer over WebSockets, local AI opponents that play at grandmaster level, and 3D engines that rival indie desktop games — all running in a Chrome tab with zero installs.
If you haven't looked at browser games recently, you're missing out. Let me walk you through what's actually worth playing right now.
Between 2015 and 2020, browser games were in a rough spot. Flash was dying, HTML5 Canvas was limited, and mobile gaming ate the casual market. Everyone assumed "real" games needed downloads.
Then three technologies matured simultaneously:
WebGL 2.0 and WebGPU gave browsers access to GPU-accelerated 3D rendering. We're talking textured environments, particle effects, dynamic lighting — things that used to require Unity or Unreal. Modern browser engines can push serious polygons.
WebAssembly let developers compile C/C++/Rust game logic to run at near-native speed in the browser. Game AI that used to require a local binary now runs entirely client-side. Chess engines, physics simulations, pathfinding algorithms — they all work at full speed without downloading anything.
WebSocket connections enabled real-time multiplayer. Not the turn-based-over-HTTP kind, but actual low-latency connections where you see other players move in real time. This unlocked everything from board game lobbies to competitive FPS matches.
The result? A wave of browser games that would have required Steam o
I keep a spreadsheet. Every January, I open it and list every software subscription I'm paying for, how often I actually use it, and what it costs per use. Last year, the spreadsheet had 23 entries. The total was $287/month — $3,444/year — and I was using half of these tools less than once a week.
So I did something radical: I cancelled everything I could replace with a browser tab.
Twelve months later, I'm down to four paid subscriptions. The rest? Browser-based tools that do the same job for exactly $0, require no installation, and — this is the part that surprised me — often work better because they process everything locally on my machine.
This isn't a listicle of random websites. This is a battle-tested collection of 100 browser-based tools organized by what expensive software they replace. Every tool here meets my criteria: no mandatory signup, no watermarks, no uploading files to mystery servers.
Let's go category by category.
Adobe Acrobat Pro costs $22.99/month. That's $276/year for the privilege of editing PDF files. And honestly, most people use maybe 10% of Acrobat's features. Here are the browser-based tools that cover the other 90%:
PDF Merge — Combine multiple PDFs into a single document. Drag, drop, reorder, done. No page limits, no watermarks, no file size caps.
PDF Split — Extract specific pages or split a PDF into individual files. You'd be amazed how many people pay for Acrobat just for this.
PDF Compress — Reduce file size without visible quality loss. Crucial for email attachments and web uploads.
PDF to Word — Converts PDF to editable DOCX format with layout preservation. The formatting fidelity has gotten shockingly good.
PDF to Image — Export each page as PNG or JPEG. Useful for presentations, social media, and documentation.
Image to PDF — Batch convert photos and screenshots
Your application works perfectly on your laptop. It handles your demo beautifully. Your investor pitch is flawless. Then real users show up, and everything you thought you knew about software engineering turns out to be a fairy tale you told yourself while localhost returned responses in 3ms.
Scaling an application from near-zero to over a million active users is humbling. The bottlenecks are never where you expect them. The failures are never what you planned for. And the solutions that work at 10K users will actively hurt you at 100K.
This is the playbook worth having before the first 3 AM wake-up call. Every code example is realistic for production. Every recommendation comes from painful experience, not from reading somebody else's blog post about "web scale."
Scaling is not a continuous process. It is a series of phase transitions. Your application works fine, works fine, works fine, and then suddenly it does not. Understanding these breakpoints saves you months of reactive firefighting.
At this stage, your single server handles everything. PostgreSQL, your application, Redis, maybe even Nginx — all on the same box. Response times are fast because there is no contention. The database returns queries in microseconds because the entire dataset fits in the buffer cache. You feel smart.
The only thing that matters here: do not add complexity you do not need yet. Plenty of teams set up Kubernetes clusters for 50 users. They spend three months on infrastructure and zero months on product. Then they shut down because nobody wanted the product.
// At 100 users, this is your entire infrastructure
// One server, one database, one process
import express from "express";
import { Pool } from "pg";
const pool = new Pool({
host: "localhost", // yes, localhost. the db is on the same box
max: 10, // 10 connections is plenty
idl
A junior developer stares at a bug for six hours. They add console.log after console.log, restart the server forty times, change random things hoping something works. Eventually they find it — or more accurately, they stumble into the fix without understanding why it works. A senior developer looks at the same bug, asks three questions, reads one stack trace, and has a fix in twenty minutes.
This is not talent. This is not "10x engineer" mythology. It is a learnable skill, and almost nobody teaches it explicitly.
I have spent twelve years debugging production systems. E-commerce platforms processing millions of dollars. Real-time collaboration tools. Payment systems where a bug means someone does not get paid. What I have learned is that debugging is not about tools — it is about thinking. The tools help, but without the right mental model, the fanciest debugger in the world will not save you.
This is everything I know about finding bugs fast.
Here is how most developers debug: something breaks, they open the file where they think the problem is, they add a console.log, they re-run the code, they look at the output, they move the console.log somewhere else, repeat. This is the shotgun approach — fire in every direction and hope you hit something.
The shotgun approach has three fatal problems:
It does not scale. When you have ten lines of code, adding random logs works. When you have ten thousand lines, or a hundred microservices, or a race condition that only reproduces under load — you are lost. You are adding logs to a haystack hoping to find a needle.
It does not teach you anything. Even when you stumble onto the fix, you often do not understand why it was broken. Which means you will write the same bug again. And the next time it manifests differently, you will not recognize it.
It is catastrophically slow. I timed this once. A junior on my team spent four hours debugging a t
Every week, someone on Twitter posts a thread about their AI agent that "autonomously" does something impressive. Books a flight. Writes and deploys code. Manages their email. The demo looks magical. The replies are full of fire emojis and "the future is here."
What nobody shows you is the version that ran before the recording. The one that booked three flights to the wrong city. The one that deployed code with a SQL injection vulnerability. The one that replied to the CEO's email with a hallucinated quarterly report. AI agents are easy to demo and brutally hard to ship.
I have spent the last eight months building AI agents for production use — not toy demos, not proof of concepts, but agents that handle real tasks for real users with real consequences when they fail. What follows is everything I have learned about the architecture, the patterns, the failure modes, and the engineering discipline required to build agents that actually work.
This is not a tutorial where we build a chatbot and call it an agent. This is the hard stuff. The stuff that determines whether your agent is a product or a party trick.
Let me start with precision, because the industry has made "agent" meaningless through overuse. Every chatbot, every RAG pipeline, every wrapper around a single API call now calls itself an agent. That is marketing, not engineering.
An AI agent, in the engineering sense, has four properties:
A chatbot has none of these. A RAG pipeline has maybe one (tool use, if you squint). A function-cal
Something quietly shifted in 2025, and most developers missed it. The way people find information on the internet changed — not gradually, but in a hard pivot. According to SparkToro's 2025 data, nearly 60% of Google searches now end without a click. Gartner's prediction that organic search traffic would drop 25% by 2026 is playing out almost exactly on schedule. And the reason is not that people stopped searching. It is that AI started answering.
ChatGPT, Perplexity, Google AI Overviews, Microsoft Copilot, Arc Search — these are not future threats. They are current reality. Your content is being consumed, summarized, and cited (or not cited) by large language models right now. And if your site is optimized only for traditional SEO, you are optimizing for half the game.
This is a technical guide. Not "add keywords to your headings" advice. I am going to show you, with actual code and architecture decisions, how to build a site that performs in both traditional search and generative AI search. Because in 2026, you need both.
Generative Engine Optimization (GEO) is the practice of structuring your content and technical architecture so that AI-powered search engines are more likely to surface, cite, and accurately represent your content in their responses.
Traditional SEO asks: "How do I rank on page one?" GEO asks: "How do I become the source that AI cites when answering a question?"
These are fundamentally different problems. Google's ranking algorithm evaluates signals like backlinks, domain authority, page speed, and keyword relevance. An LLM generating an answer evaluates something closer to: "Is this content authoritative, well-structured, directly relevant, and easy to extract factual claims from?"
There is significant overlap. Good content helps in both cases. But the technical implementation diverges in important ways, and developers are uniquely positioned to get this right because GEO is a
I have been writing JavaScript professionally for years. I think in closures. I dream in Promise.all. My muscle memory types const before my brain even processes what variable I need. JavaScript is the water I swim in.
And then I tried to learn Rust, and the water turned into concrete.
Not because Rust is bad. Not because it is unnecessarily complex. But because Rust forced me to confront assumptions about programming that I did not even know I had. Assumptions about memory. About who owns data. About what it means for a value to "exist." JavaScript lets you ignore all of these questions. Rust makes them the entire point.
This post is an honest retelling of that journey. What broke my brain, what eventually clicked, and why — against all expectations — learning Rust made me significantly better at writing JavaScript.
Let me be upfront about motivation, because "learn Rust" is not something most JS developers wake up wanting to do.
My reasons were practical. I had a computationally intensive process running in Node.js that was too slow. I needed to ship a CLI tool that could not require users to install Node. I was curious about WebAssembly and wanted to write something that compiled to it without fighting C++ toolchains.
Rust kept showing up as the answer to all three problems. And unlike C or C++, Rust promised that I would not spend my weekends debugging segmentation faults and memory corruption. The compiler would catch those bugs before I could even run the code.
That promise turned out to be true. But the cost of that promise — the mental overhead of satisfying the Rust compiler — is something nobody adequately prepares you for.
In JavaScript, you never think about who "owns" a value. You create an object, pass it to a function, store it in three different arrays, close over it in a callback, and the garbage collector sorts everything out. You do
A confession to start with. It is easy to convince a team to go "fully serverless" for a new product. No containers. No VMs. Pure Lambda, DynamoDB, SQS, Step Functions — the works. The architecture diagrams look beautiful. The pitch to leadership is compelling: "We only pay for what we use. It scales to zero. It scales to infinity."
Eighteen months later, the monthly bill stabilizes at over $20,000. The equivalent workload on three well-provisioned VPS instances would cost roughly $600. The traffic — about 2 million requests per day — sounds like a lot until you realize that's about 23 requests per second on average, a workload any $200/month server handles while yawning.
This post collects lessons from that kind of experience and the serverless projects that follow it. Some genuinely benefit from serverless. Many do not. The difference is understanding the patterns, the anti-patterns, and most critically, the cost model.
Before we get into patterns, let's fix the mental model. When most developers hear "serverless," they think "functions as a service" — write a function, deploy it, it runs when triggered. That's accurate but incomplete, like saying a car is "an engine with seats."
Serverless is a deployment and execution model where:
That last point is the one people underestimate. Everything you know about in-process caching, connection pooling, background threads, and local file storage needs to be rethought. Your function is a stateless transformer: input comes in, output goes out, anything you want to remember lives somewhere else.
Traditional Server Mental Model:
[Long-lived process] -> in-memory cache
-> connection pool
-> local filesystem
I have reviewed more authentication pull requests than I care to count. Not just my own code -- code from teams at startups, mid-sized companies, and a few Fortune 500 engineering orgs. And here is the uncomfortable truth: almost every codebase I have audited has at least one critical authentication vulnerability. Not edge cases. Not theoretical attacks. Real, exploitable holes that would let an attacker impersonate users, steal sessions, or escalate privileges.
The problem is not that developers are careless. The problem is that authentication is taught badly. Every blog post says "use JWTs" or "use sessions" without explaining the actual tradeoffs. Every OAuth tutorial glosses over the parts that matter. And the security implications of each decision are buried in RFCs that nobody reads.
This post is different. I am going to walk through every major authentication pattern, explain the tradeoffs that actually matter in production, and share the mistakes I have seen (and made) so you can avoid them. Fair warning: I have strong opinions. I have earned every single one of them the hard way.
Let me start with the debate that consumes more engineering hours than it should. Sessions versus JWTs. The way this debate usually goes is: someone on the team says "JWTs are stateless and scale better," someone else says "sessions are more secure," and then the team picks whichever one the most senior engineer prefers, without actually understanding the tradeoffs.
Both sides are wrong in the ways that matter most.
A session-based system works like this: the user logs in, the server creates a session record (in a database, Redis, or some other store), and sends back an opaque session ID in a cookie. On every subsequent request, the server looks up the session ID and loads the associated data.
import { randomBytes } from "crypto";
interface SessionData
I spent five years writing SQL before I truly learned window functions. Five years of correlated subqueries, self-joins, temporary tables, and application-layer gymnastics to solve problems that window functions handle in a single, readable expression. When I finally sat down and learned them properly, I had a genuine "why did nobody tell me about this" moment. I went back to old code and rewrote queries that had been bothering me for years. Half of them got shorter. Most of them got faster. A few went from "we run this overnight in a batch job" to "this returns in 400 milliseconds."
Window functions are the most powerful feature in SQL that most developers skip. They show up in interview prep, get filed under "advanced," and then ignored in day-to-day work. That's a mistake. Once you understand them, you'll reach for them constantly — running totals, rankings, moving averages, detecting gaps, sessionizing user events, computing year-over-year growth. All without subqueries. All in a single pass over the data.
This post is everything I wish someone had told me about window functions five years ago. Not the documentation. The real patterns, the real use cases, and the performance traps that don't show up in tutorials.
Here's the conceptual shift that took me too long to grasp: a regular aggregate function like SUM() or COUNT() collapses rows. You put in 1,000 rows, you get back 1 (or a few, with GROUP BY). A window function computes across a set of rows but keeps every row in the result. That's it. That's the whole idea.
-- Regular aggregate: collapses rows
SELECT department, SUM(salary)
FROM employees
GROUP BY department;
-- Result: one row per department
-- Window function: keeps all rows, adds the computation
SELECT name, department, salary,
SUM(salary) OVER (PARTITION BY department) as dept_total
FROM employees;
-- Result: every employee row, with their department's total attac
It is common to spend the first three months with Kubernetes convinced it is over-engineered nonsense designed by people who had never actually shipped an application. A Node.js API, a Postgres database, and a Redis cache deploy fine on a single VPS with Docker Compose in 30 seconds. Then a "cloud-native" mandate arrives, and suddenly there are YAML files everywhere, fights with ingress controllers, and 2 AM "CrashLoopBackOff" debugging.
After enough time, Kubernetes earns appreciation for what it does — but with strong opinions about when it is appropriate and when it is a spectacular waste of engineering time. This post is the kind of orientation that helps on day one — written for application developers, not cluster administrators.
If you are here because your team just adopted Kubernetes and you need to understand it fast, start at the top. If you already know the basics and want the debugging workflow, skip to the kubectl section.
The official explanation involves "container orchestration" and "declarative desired state management." That is technically accurate and practically useless for understanding why you should care.
Here is the actual problem Kubernetes solves: you have containers, and you need them to run reliably across multiple machines without you manually managing each one.
Before Kubernetes, deploying a containerized application to multiple servers meant writing custom scripts. You SSH into a server, pull the new image, stop the old container, start the new one. If you have three servers, that is three SSH sessions. If one fails, you need to detect that and handle it yourself. If traffic spikes and you need more instances, you spin up a new server and repeat the process manually. If a container crashes at 3 AM, it stays down until someone notices and restarts it.
Kubernetes handles all of that. You tell it "I want five copies of this container running at all times" and it makes that ha
Every tutorial on integrating LLMs into web apps follows the same script: install the SDK, call the API, display the response. Twenty lines of code, maybe thirty. "Look how easy it is!" And it is easy — until you ship it to real users and discover that your AI feature costs $400/day, times out on 12% of requests, occasionally tells users to eat glass, and streams responses so slowly that people close the tab before they see the answer.
I have spent the better part of a year building AI-powered features into production web applications. Not chatbot wrappers. Not demo projects. Features that handle real traffic, run against real cost constraints, and have to work reliably for users who do not care that there is an LLM behind the curtain — they just want the thing to work.
What follows is everything I have learned about doing this properly. The architecture decisions, the streaming implementation details, the cost traps, the failure modes, and the UX patterns that make the difference between an AI feature that delights users and one that embarrasses you.
Before you write a single line of code, you need to decide where the LLM sits in your architecture. This sounds obvious, but the wrong decision here will haunt you for months.
Option 1: Direct API calls from your backend. Your Next.js API route calls the LLM provider directly. Simple, fast to implement, and the right choice for most teams starting out.
Option 2: A dedicated AI service layer. A separate service (or at least a separate module) that owns all LLM interactions. Prompt templates, model selection, response parsing, caching — all in one place. More work upfront, dramatically easier to maintain.
Option 3: Edge functions. Running LLM calls at the edge sounds appealing for latency. In practice, edge runtimes have strict execution time limits and memory constraints that make them a poor fit for most LLM workloads. The LLM API call it
I have built and maintained production APIs in both REST and GraphQL for years now. Not toy projects. Not tutorial apps. Real systems with real users, real performance budgets, real on-call rotations, and real 3 AM incidents that taught me things no blog post ever could.
And after all of that, my honest take is this: the internet's favorite API debate is almost entirely missing the point. The question isn't "which is better." The question is "which set of tradeoffs are you signing up for, and do you actually understand what those tradeoffs mean at 2 AM when something breaks?"
Let me walk you through every dimension of this comparison, with code, with war stories, and with the kind of honesty that conference talks and documentation pages are allergic to.
Every GraphQL pitch starts here, so let me address it first. The claim: REST APIs return too much data (over-fetching) or require multiple requests to assemble what you need (under-fetching). GraphQL solves both by letting the client specify exactly what it wants.
Here's the REST endpoint everyone uses as the punching bag:
// GET /api/users/123
{
"id": 123,
"name": "Alex Kousa",
"email": "alex@example.com",
"bio": "Software engineer who actually reads error messages.",
"avatarUrl": "https://cdn.example.com/avatars/123.jpg",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2026-01-10T14:30:00Z",
"settings": {
"theme": "dark",
"language": "en",
"notifications": true,
"twoFactorEnabled": true
},
"stats": {
"postsCount": 47,
"followersCount": 1203,
"followingCount": 89
}
}
And here's the GraphQL version where the client only asks for what it needs:
query {
user(id: 123) {
name
avatarUrl
}
}
Clean. Elegant. Undeniably less data over the wire. The GraphQL evangelists win this slide every time.
But here's what they don't
I tried Deno when it first launched in 2020. Wrote a small CLI tool with it, appreciated the TypeScript-out-of-the-box experience, hit a wall when I needed a library that only existed on npm, and went back to Node.js. That was the Deno 1 experience for a lot of people. Great ideas, not enough ecosystem.
Deno 2 is a fundamentally different proposition. Ryan Dahl and the Deno team looked at the adoption numbers, acknowledged the cold reality that you cannot build a JavaScript runtime without npm compatibility, and rebuilt the entire story around Node.js interop. They also launched JSR, a new registry designed for TypeScript-first packages. And they kept all the things that made Deno interesting in the first place: the security model, the built-in tooling, the web standard APIs.
I've spent the last few months using Deno 2 for real work. Not toy projects. An API server that handles production traffic. A CLI tool that replaced a Node.js script I'd maintained for two years. A Fresh application that serves actual users. This post is what I learned.
Before we talk about Deno 2, let's be honest about what went wrong with Deno 1. Because understanding the failure mode matters for evaluating whether Deno 2 actually fixes it.
Deno 1 had a radical vision: no node_modules, no package.json, no npm. You imported modules via URLs. TypeScript was native. The permission system was genuinely innovative. The standard library was curated and high quality.
The problem was practical. Most JavaScript developers don't start projects from scratch. They assemble them from packages. And those packages lived on npm. Deno 1's answer was "use a CDN like esm.sh or deno.land/x," which worked for simple cases and fell apart for anything complex. Try importing a package that depends on Node.js built-in modules like fs or path through a URL import. It doesn't work, or it works through a compatibility layer that introduces subtle bugs.
The imp
Every WebSocket tutorial starts the same way. Open a connection, send a message, receive a message, celebrate. Thirty lines of code and you've got "real-time." Congratulations. Now deploy that to production, put it behind a load balancer, handle 10,000 concurrent connections, deal with flaky mobile networks, and watch everything fall apart.
I've built real-time features across multiple projects — multiplayer games, live dashboards, collaborative editors, notification systems, status monitoring. The pattern is always the same: the initial implementation takes a day. Making it reliable takes weeks. Making it scale takes months. And the gap between "works on localhost" and "works in production" is where most teams lose their minds.
This is everything I've learned about WebSockets in production. Not the happy path. The real path — with reconnections, heartbeats, scaling headaches, and the subtle bugs that only show up when thousands of users are connected at 2 AM.
A WebSocket connection isn't just "open" or "closed." In production, you're dealing with at least six distinct states, and most applications only handle two of them:
type ConnectionState =
| "connecting" // Handshake in progress
| "connected" // Open and healthy
| "stale" // Open but no recent heartbeat
| "reconnecting" // Actively trying to reconnect
| "disconnected" // Clean close or max retries exceeded
| "suspended"; // Tab backgrounded, connection paused
The browser's WebSocket API gives you four events: onopen, onmessage, onclose, onerror. That's it. No "stale" detection. No "reconnecting" state. No visibility change handling. You build all of that yourself, or you suffer.
Here's what a proper connection manager looks like. Not a toy — something that actually survives the real world:
interface ConnectionConfig {
url: string;
protocols?: string[];
heartbeatInterval
Most application developers treat Linux like a black box. You deploy your code, it runs, and when something goes wrong you Google the error message and paste Stack Overflow commands until things work again. That works for years — until production services on bare metal VPS instances start biting back, hard.
A Node.js process gets killed with no error message. A Go service runs out of file descriptors at 3 AM. A Python app is somehow using 4 GB of virtual memory despite holding a 200 MB dataset. A Docker container creates zombie processes that slowly eat the PID space. A server stops accepting connections even though CPU and memory look fine.
Every one of these problems has a clear, explainable cause rooted in Linux internals. Not obscure kernel-hacker-only internals — the fundamentals that the operating system is built on. Once you understand them, these problems go from mysterious to obvious. More importantly, you can prevent them instead of reacting to them.
This is not an operating systems textbook. This is the kind of orientation worth having before learning it the slow way through production incidents.
Every process on Linux begins with a single system call: fork(). When a process calls fork(), the kernel creates an almost identical copy of the calling process. The child gets a new PID, but it inherits a copy of the parent's memory space, file descriptors, signal handlers, and environment. The key word is "copy" — the child is independent. Changing memory in the child does not affect the parent, and vice versa (though the kernel is clever about this, as we will see with copy-on-write).
After fork(), the child process typically calls exec() to replace itself with a different program. This is the fork-exec pattern, and it is how virtually every program gets launched on Linux. When you type ls in your terminal, bash calls fork() to create a child, then the child ca
I have broken production APIs more times than I am comfortable admitting. Not because I wanted to ship breaking changes, but because I did not understand what "breaking" meant to the people consuming my API. To me, renaming a field from userName to username was a cleanup. To the mobile team shipping a build through App Store review, it was a week of emergency patches and a very tense Slack thread.
API versioning sounds like a solved problem. Pick a strategy, slap /v1/ on your URLs, and move on. In practice, it is one of the most consequential architectural decisions you will make, and the consequences do not show up until you have real consumers, real data, and real pressure to evolve without breaking things.
This is everything I have learned about API versioning from running APIs in production for eight years. Some of it came from good decisions. Most of it came from bad ones.
The textbook explanation of API versioning is mechanical: you have version 1, you make breaking changes, you create version 2. Simple. What the textbooks do not tell you is that API versioning is fundamentally a social contract, not a technical one.
When you publish an API, you are making a promise. You are saying: "If you build against this, it will keep working." The moment someone writes code that depends on your response shape, your status codes, your error format, even your field ordering in some unfortunate cases, you have a consumer who trusts you.
Breaking that trust is expensive. Not in the "we need to update our SDK" sense, but in the "our customer's production system went down at 2 AM and they are reconsidering whether to use our platform" sense. I have been on both sides of that phone call.
The hard part of API versioning is not the routing or the URL scheme. It is answering these questions:
Most developers have a rough mental model of how browsers render pages. HTML goes in, pixels come out, something something DOM, something something paint. That mental model is good enough until it isn't — until you're staring at a janky scroll, a layout shift you can't explain, or a mysterious 200ms gap in your Performance trace where the main thread is just... blocked.
It takes years to build that rough mental model into something precise. Reading browser engine source code is not a strict requirement, but every hard performance bug worth fixing requires understanding what the browser is actually doing, not what you assume it is doing.
This is the deep dive worth having years ago, not today. The plan: trace the entire path from raw bytes arriving over the network to photons leaving the monitor, and at every step, point out where things go wrong and how to fix them.
Here's the pipeline every browser follows, whether it's Chrome, Firefox, or Safari:
Each step has its own constraints, its own performance characteristics, and its own set of gotchas. Let me walk through the ones that matter.
The critical insight is that this is not a simple linear pipeline. Steps overlap. The HTML parser can be working on step 4 while CSS is still downloading for step 6. The preload scanner (more on this shortly) is speculating about future resources while the main parser is blocked. Understanding these overlaps is the difference betwee
After dozens of system design interviews — on both sides of the table — a pattern keeps repeating: candidates memorize the "correct" architecture for a URL shortener or a chat application, then crumble the moment a single constraint changes. They have learned answers. They have not learned how to think.
Most system design prep material follows the same formula: here is the problem, here is the solution diagram, memorize it. This approach fails spectacularly in real interviews because interviewers are not testing whether you can regurgitate a reference architecture. They are testing whether you can reason about trade-offs under ambiguity, communicate clearly while exploring a design space, and make defensible decisions with incomplete information.
What follows is the kind of orientation that pays off before a first system design interview — the mental models, the math, the patterns, and the mistakes that separate candidates who get offers from candidates who get polite rejections.
The typical study path looks like this: watch a YouTube video about designing Twitter, read a blog post about designing Uber, memorize the components, and hope the interview asks about something similar. This is like studying for a math exam by memorizing the answers to specific problems instead of understanding the underlying concepts.
System design interviews are open-ended conversations. The interviewer gives you a deliberately vague prompt — "design a notification system" — and watches how you navigate from vagueness to clarity. The skills being evaluated are:
Scoping and clarification. Do you ask what kind of notifications? Push, email, SMS, in-app? What scale? What latency requirements? Or do you just start drawing boxes?
Trade-off analysis. When you choose a database, can you articulate why you chose it over alternatives? When you add a cache, do you acknowledge the consistency implications?
**Iterative refin
I have implemented OAuth integration in production systems for over a decade. I have read the RFCs cover to cover -- multiple times, because they keep publishing new ones. I have debugged token flows at 2 AM when a misconfigured redirect URI brought down authentication for an entire platform. And after all of that, I can tell you with absolute certainty: OAuth is confusing on purpose. Not maliciously, but structurally. It is a framework designed by committee to solve a very broad problem, and the resulting specification leaves enough ambiguity to keep security consultants employed for decades.
This post is not another "OAuth in 5 minutes" tutorial. Those tutorials are why developers get OAuth wrong. Instead, I am going to walk through every part that I have seen trip up experienced engineers -- the parts where reading the spec once is not enough, where the mental model most developers carry is subtly broken, and where subtle bugs turn into CVEs.
This is the single most important thing to understand about OAuth, and the thing most developers get wrong. OAuth 2.0 is an authorization framework. It answers the question: "Should this application be allowed to access this resource on behalf of this user?" It does not answer: "Who is this user?"
The distinction matters enormously. When you do the OAuth dance with Google and get back an access token, that token says "the bearer of this token is allowed to read this user's email." It does not say "the bearer of this token is John Smith with email john@example.com." You might think, "But I can call the /userinfo endpoint with that token and get the user's identity." And you can. But that is not part of the OAuth spec. That is OpenID Connect, which is a separate specification built on top of OAuth. When you conflate the two, you end up with authentication systems that have authorization-shaped holes.
Here is the concrete problem. Suppose you buil
Plenty of engineers have been paged at 3 AM more times than they care to count. Some of those incidents resolve in four minutes because the telemetry says exactly what is wrong. Others take four hours because somebody is staring at a wall of unstructured logs trying to correlate timestamps by hand across six different services. The difference is rarely talent or experience. It is almost always the quality of the observability stack.
This post is not about observability in a single language or framework. There is a separate companion post about Node.js observability specifically. This one is about the full picture: the architecture decisions, the storage backends, the alerting philosophy, the cost traps, and the debugging workflow that ties it all together.
Every observability vendor presentation starts the same way: "There are three pillars of observability: logs, metrics, and traces." They draw three columns on a slide, put a checkmark next to each one, and move on to pricing.
Here is the problem: three isolated pillars are just three isolated data silos. Having logs in Elasticsearch, metrics in Prometheus, and traces in Jaeger does not give you observability. It gives you three different tools you have to manually cross-reference while your pager is screaming.
The actual value of observability comes from correlation. When a metric shows a latency spike at 03:14, you need to click on that spike and see the traces that contributed to it. When you find a slow trace, you need to see the logs emitted during that trace. When a log shows a database error, you need to see the metric that tells you how many other requests hit the same error.
Without correlation, you have three separate haystack-searching tools. With correlation, you have a unified debugging experience.
The missing piece that makes correlation work is context propagation. Every request entering your system gets a trace ID. That trace ID flows throu
Picture a Friday afternoon — because it is always a Friday afternoon — when a junior developer runs a migration on the main orders table. The migration looks perfectly innocent: add a NOT NULL column with a default value to a table with 80 million rows. In PostgreSQL 10, this meant a full table rewrite. The entire table locks for 3 hours and 47 minutes. Every API endpoint that touches orders returns 500s. The on-call engineer's phone explodes. The estimated cost in failed transactions: hundreds of thousands of dollars.
The migration itself? Five lines of SQL. The postmortem? Forty-seven pages.
After enough years of production database migrations across PostgreSQL, MySQL, and a painful stint with SQL Server, the same lesson keeps appearing: the migration that terrifies you is rarely the one that causes the outage. It is the "simple" one that nobody reviews because it looks trivial. An ALTER TABLE here, an ADD COLUMN there. What could go wrong?
Everything. Everything can go wrong.
This post collects the patterns, tool recommendations, and warnings that come from real production incidents.
To understand why schema changes cause outages, you need to understand how PostgreSQL handles locks. Every DDL statement acquires an ACCESS EXCLUSIVE lock on the table. This is the strongest lock type in PostgreSQL's lock hierarchy. While this lock is held, nothing else can read from or write to that table. Not SELECT, not INSERT, not UPDATE. Nothing.
Here is where it gets really nasty. Even if your ALTER TABLE is fast — say, adding a nullable column with no default, which is metadata-only and takes milliseconds — it still needs to acquire that ACCESS EXCLUSIVE lock. And it has to wait in the lock queue behind any currently running queries. And while it is waiting, it blocks every new query that comes in behind it.
Let me illustrate with a timeline:
T=0s Long-running SELECT starts (holds ACCESS SHARE lock)