Mert
ADASO Başkanı Kıvanç: “Teknolojik devrim yaşanıyor”
BİM marketler Samsung Galaxy A17, Nokia 105 ve Xiaomi Redmi 15C satacak
22 Mayıs BİM aktüel teknoloji ürünleri arasında Nokia 105 tuşlu telefon, Xiaomi Redmi 15C ve Samsung Galaxy A17 5G akıllı telefon modelleri yer alacak.Where React for IaC came from
This is the first technical post in the Dinghy series, and it is the one I had to write before any of the others made sense. It is the origin story of the project — how a Terraform codebase that had grown beyond what copy-paste could maintain ended up being rendered out of TSX — and the core concept the rest of Dinghy is built on.
How I ended up here
Back in the day, I was maintaining a Terraform codebase across several environments. Most of the configuration was duplicated between them — the same resources, the same modules, repeated for every environment with only the inputs changing. Every change had to land in every environment. Every difference between them was either a deliberate choice or a bug waiting to happen.
Modules helped. For a while. Then the modules themselves became the maintenance burden — a layer of indirection that still couldn’t express what I actually wanted to say.
HCL is HashiCorp Configuration Language. The keyword is configuration. It describes resources. It does not let you reason about them.
I wanted a real language.
The first move: CDKTF
CDKTF — Cloud Development Kit for Terraform — was the obvious next stop. Write infrastructure in TypeScript, render to Terraform JSON, deploy with the regular Terraform tooling. I tried it. It did what it said on the tin: real loops, real functions, real imports, real IDE support.
But two things kept nagging.
Construct boilerplate. CDKTF inherits AWS CDK’s construct pattern: new Vpc(this, 'vpc', {...}), then new Subnet(this, 'subnet', { vpc, ... }). Every resource needs a parent scope and a unique id, hand-passed. It is faithful to AWS CDK, but it never stopped feeling like extra steps around the thing I actually wanted to write.
Cross-resource wiring. When two resources needed to talk to each other, I was still threading handles by hand. Pass the VPC into the subnet. Pass the subnet into the EC2. Pass the EC2 into the security group rule. The hierarchy in my head — VPC contains subnets contains instances — was nowhere visible in the source.
CDKTF had solved the programmable-language problem. It had not, for me, solved the expressing nesting problem.
The click: TSX nests the way infrastructure nests
When you draw a cloud architecture diagram, you naturally nest things inside containers — the VPC box wraps the subnet boxes; the subnet boxes wrap the instances. That nesting is how the infrastructure composes.
The moment that turned this into a project rather than a complaint was noticing that TSX is already nested in the same way. When you write:
<Vpc>
<Subnet>
<Ec2 />
</Subnet>
</Vpc>
The source code shape matches the runtime shape. The thing you write looks like the thing you mean. No new X(parent, id, {...}). No explicit handle threading. The parent is right there — it is the JSX element containing you.
The next step was obvious. React looks like a tool for building web pages, but underneath, it is a general-purpose engine for building trees. The part that turns your TSX into HTML doesn’t actually need to produce HTML — it can produce anything, as long as you tell it how. Tell it to produce Terraform JSON, and that is what you get.
Don’t worry about React’s complexity
If React’s hooks, lifecycle, and state-management debates have put you off, you can relax. Dinghy uses almost none of that.
Dinghy renders once. You write the TSX, Dinghy walks the tree from top to bottom one time, writes the Terraform JSON, and stops. No reactivity. No re-renders. No state to manage. No lifecycle.
So none of the modern React you might have struggled with applies here:
-
— no local stateuseState -
— no side effects or lifecycleuseEffect -
— nothing to memoiseuseMemo
Dinghy only keeps the bits that make the nesting work:
- TSX — the authoring syntax
- The tree-walker that turns TSX into output
-
useContext— for letting a child component reach a surrounding parent (the bucket trick you will see in the next section)
Most of what React has accumulated for browser UI doesn’t apply here, and you don’t need to know it.
Context fixes the wiring problem
Here is the bit that finally felt right. With CDKTF I was threading a bucket handle down through props. With Dinghy:
const BucketVersioning = () => {
const { s3Bucket } = useAwsS3Bucket()
return (
<AwsS3BucketVersioning
bucket={s3Bucket.bucket}
versioning_configuration={{ status: 'Enabled' }}
/>
)
}
<AwsS3Bucket ...>
<BucketVersioning />
</AwsS3Bucket>
BucketVersioning does not need to be told which bucket it is enabling versioning on. It asks the surrounding AwsS3Bucket via context. Drop it anywhere under a bucket and it Just Works.
That is the kind of composition HCL fundamentally cannot give you. CDKTF can do it, but only by passing the parent reference by hand.
Beyond Web
The other thing that fell out of choosing React: the output can be anything. React doesn’t care whether the result is HTML, native mobile widgets, terminal UI, or Terraform JSON. Point it at a different kind of output, and it produces that.
React started as a web library, but it has long since moved beyond the web:
- Web — the original
- Native — iOS and Android apps
- CLI — Ink, Claude Code, and similar tools render terminal interfaces with React-like trees
- Infrastructure — Dinghy, rendering diagrams and Terraform
Same model, different output. That is why a single engine can sensibly cover diagrams and infrastructure under one roof — what the tree renders to is just a detail.
Up next: Diagram as Code with draw.io — the same engine on a simpler problem, before the advanced Infrastructure as Code story.
Breaking Down Browser DevTools Until It Finally Made Click
Introduction
Welcome to the 3rd episode of my series “Sunday Source – I Break It, Then Explain It”.
Yesterday, I attended a mock interview at my Payilagam institute, and the interviewer Prithivraj asked me a simple but tricky question:
“In a browser, there is a button. On click, where do you see that onclick function?”
For a moment, I paused. I knew I had seen it many times — I right-click and inspect daily — but I couldn’t immediately recall which section shows it. My mind jumped to the Console first, but that wasn’t the correct place.
That moment made me realize something important: we use DevTools daily, but we don’t always understand where things actually live.
So I decided to break it down properly — every panel, every section, and how the browser really exposes these internals.
This article is the result of that breakdown.
Modern web applications are complex systems made of HTML, CSS, JavaScript, APIs, and browser engines working together. To understand and debug this system, browsers provide a built-in engineering toolkit called Developer Tools (DevTools).
A widely used implementation is Chrome DevTools, which exposes the internal state of the browser in real time.
Think of DevTools as:
A live interface into the browser’s rendering engine, JavaScript runtime, and networking layer.
0. What DevTools Actually Is (Internal View)
Before individual panels, it’s important to understand what DevTools sits on top of:
The browser has 4 core systems:
- Rendering Engine → Builds and paints UI (DOM + CSS)
- JavaScript Engine → Executes JS (V8 in Chrome)
- Networking Layer → Handles requests (HTTP/HTTPS)
- Storage Layer → Cookies, cache, localStorage
DevTools connects to all of them:
DevTools
├── DOM Inspector → Rendering Engine
├── Console → JavaScript Engine
├── Network → Networking Layer
├── Application → Storage Layer
└── Performance → All systems combined
So DevTools is NOT just a UI tool — it is a debug bridge into browser internals.
1. Elements Panel (DOM + CSS Rendering System)
The Elements panel shows the live DOM tree, not the original HTML file.
Important Concept: DOM ≠ HTML
When a page loads:
HTML file → Parsed → DOM Tree → Rendered UI
JavaScript can modify DOM dynamically:
document.createElement()
document.appendChild()
innerText changes
So what you see in Elements panel is:
“Current live state of the page”
Inspecting live DOM structure and CSS rules inside Chrome DevTools Elements panel
Deep Diagram: Rendering Pipeline
HTML → DOM
CSS → CSSOM
↓
Render Tree
↓
Layout (positioning)
↓
Paint (pixels)
↓
Composite (final screen)
Elements panel lets you inspect DOM + CSSOM interaction indirectly.
Real Debug Scenario
Imagine a button is not visible.
You inspect and find:
button {
display: none;
}
OR
opacity: 0;
visibility: hidden;
OR even:
position: absolute;
top: -9999px;
👉 DevTools helps you instantly identify why something is not visible.
Live CSS Editing (How it really works)
When you edit:
color: red;
DevTools:
- Updates CSSOM in memory
- Triggers re-render
- Recalculates layout
- Repaints UI
So every change is a mini rendering cycle
Advanced Use: Computed Styles
Computed tab shows:
- Final CSS after all overrides
- Inherited values
- Browser defaults
Example:
color: black (from user agent)
color: blue (your CSS)
→ final = blue
2. Console Panel (JavaScript Runtime Interface)
The Console is not just a log window — it is a direct REPL (Read Evaluate Print Loop) into the JavaScript engine.
Internal Working
You type JS → V8 Engine executes → Memory updated → Output returned
This is the same engine used in Node.js (Chrome uses V8).
Console is a full JavaScript environment
JavaScript errors, warnings, and logs displayed inside DevTools Console
You can:
1. Access DOM directly
document.body
2. Modify live page state
document.title = "Hacked via Console";
3. Store variables in memory
let x = 10;
x * 20;
Real Debug Scenario
Bug:
user.profile.name
Error:
Cannot read properties of undefined
Meaning:
user.profile is undefined → name access fails
Console helps trace:
- Which variable is undefined
- At which line execution failed
- What state caused it
Advanced Feature: console object
console.log()
console.warn()
console.error()
console.table()
Example:
console.table([
{ name: "A", age: 20 },
{ name: "B", age: 25 }
]);
👉 Converts JSON into readable table
3. Network Panel (HTTP Request Debugging System)
This panel shows how the browser communicates with servers.
Network waterfall showing API calls, assets, and loading timelines in real time
Internal Flow
JS fetch() → Browser Network Layer → DNS → Server → Response → Browser
What DevTools captures
Each request includes:
- Request URL
- Method (GET/POST)
- Headers
- Payload
- Response
- Timing breakdown
Deep Request Timeline
DNS Lookup → TCP Connect → SSL Handshake → Request Sent → Waiting → Response → Download
Real Debug Scenario
API not working:
Status code:
404 → URL wrong
500 → Server issue
401 → Authentication missing
Example fetch breakdown
fetch("/api/user")
Network panel shows:
Request:
GET /api/user
Response:
200 OK
{"name": "Alex"}
Performance insight
You can detect:
- Slow APIs
- Large payloads
- Blocking scripts
4. Sources Panel (JavaScript Execution Engine Debugger)
This is the closest thing to “live code execution control”.
JavaScript execution paused at a breakpoint inside Chrome DevTools Sources panel, showing real-time debugging and call stack inspection
Internal Concept
JavaScript runs in a single thread:
Call Stack → Executes line by line
Sources panel lets you PAUSE this execution.
Breakpoint System
When you set a breakpoint:
JS Engine → pauses execution → freezes state → shows memory snapshot
Step-by-step control
You can:
- Step over → skip function internals
- Step into → go inside function
- Step out → exit function
Real Debug Scenario
function total(a, b) {
let sum = a + b;
return sum;
}
At breakpoint:
a = 5
b = 10
sum = 15
You can literally watch execution happen.
5. Application Panel (Browser Storage Engine)
This shows persistent and session-based storage.
Inspecting cookies, local storage, and session storage inside Application panel
Storage hierarchy
Browser Storage Layer
├── Cookies (server communication)
├── LocalStorage (permanent)
├── SessionStorage (tab-based)
├── IndexedDB (large structured data)
└── Cache Storage (offline assets)
Cookies (Important Concept)
Cookies are automatically sent with every request:
Browser → Server sends cookie → Server remembers user
Used for:
- Authentication
- Sessions
- Tracking
LocalStorage vs SessionStorage
| Feature | LocalStorage | SessionStorage |
|---|---|---|
| Lifetime | Permanent | Tab closed |
| Scope | All tabs | Single tab |
Example
localStorage.setItem("theme", "dark");
Stored permanently in browser memory.
6. Performance Panel (Browser Rendering Profiler)
This panel shows how the browser spends time rendering your page.
Performance timeline showing scripting, rendering, and painting phases of a webpage in Chrome DevTools
Internal Pipeline
JS → Layout → Paint → Composite → GPU Rendering
Flame Chart Explanation
Main Thread:
████ JS Execution
████ Layout Recalculation
███ Paint
█ Composite
Real Problems Found Here
- Layout thrashing (bad loops modifying DOM)
- Heavy JavaScript blocking UI
- Repeated re-renders
Example Issue
for (let i = 0; i < 1000; i++) {
document.body.innerHTML += "x";
}
👉 Causes massive reflow + slow performance
7. Device Mode (Responsive Rendering Engine Simulation)
This is not just resizing — it simulates mobile browser rendering behavior.
Simulating mobile device view using Chrome DevTools responsive mode
What changes internally:
- Viewport meta simulation
- Touch event simulation
- Pixel ratio changes
- CSS media queries trigger differently
Responsive Flow
Desktop Layout → Breakpoint Trigger → Mobile Layout
Example CSS
@media (max-width: 600px) {
body {
background: black;
}
}
DevTools triggers this instantly.
8. Security Panel (Trust Verification System)
This shows how safe the website connection is.
Security overview showing HTTPS encryption status and certificate validation in Chrome DevTools
Checks performed:
- SSL certificate validity
- HTTPS encryption status
- Mixed content detection
HTTPS Flow
Browser ↔ Secure Encrypted Tunnel ↔ Server
If broken:
- Warning appears
- Resources blocked
Final Mental Model
DevTools is not just UI inspection.
It is:
DOM Inspector + JS Runtime Debugger + Network Analyzer + Performance Profiler + Storage Manager
Before You Scroll Away
This is Episode 3 of my series “Sunday Source – I Break It, Then Explain It”.
That mock interview moment with Prithivraj stayed in my mind because it exposed a simple truth — we often use tools daily without fully understanding where things actually exist inside them.
The question “where do you find the onclick function in browser DevTools?” wasn’t difficult, but it tested clarity of fundamentals.
And this article became my answer to that moment.
Now I know:
- It’s not in Console
- It’s not guesswork
- It’s inside the Elements panel → Event Listeners section
More importantly, I learned something deeper:
Knowing tools is not enough — understanding their structure is what makes you confident in interviews and real debugging.
This is exactly what Sunday Source is about — breaking things down until they make sense.
See you in the next “Sunday Source” episode
Training Your Mouse Behavior Clone: Make AI Browser Agents Move Like You
In May 2026, a paper titled “FP-Agent: Fingerprinting AI Browsing Agents” was published on arXiv. The research team measured 7 mainstream AI browsing agents and found that their mouse trajectories, typing rhythms, and other behavioral features form a distinctive fingerprint — not only distinguishing AI from humans, but also differentiating between different agent frameworks.
What’s more concerning: the behavioral consistency across existing schemes makes them easy to classify as automated traffic.
This article explores, from a browser automation developer’s perspective, how to use deep learning to make AI agent mouse operations learn your style instead of applying a generic “humanization” template.
The Problem: Why Does “Humanization” Become a Fingerprint?
Most mainstream browser automation frameworks handle “humanization” of mouse movements like this:
def move_mouse(x, y):
points = bezier_curve(current_pos, x, y)
for px, py in points:
mouse.move(px, py)
time.sleep(random.uniform(0.01, 0.03))
Seems reasonable? But here’s the problem:
- Everyone using this framework generates the same class of Bezier curves
- Random jitter follows the same distribution
- Overshoot trigger probability is the same fixed value
If you run 1,000 automation instances and cluster their mouse trajectories, you’ll find they heavily overlap. This creates a behavioral fingerprint — detect one pattern, identify all instances using that framework.
The irony: the more “humanization” features you add, the less human it becomes — because there’s no unified “human” pattern. If all your automation instances share the same “humanization” parameters, those parameters themselves become a massive collective fingerprint.
A Different Approach: Learn “You”, Not “Human”
If the model is trained on your personal mouse operation data and generates trajectories with your personal style, the situation changes completely:
| Dimension | Generic Humanization | Personal Behavior Clone |
|---|---|---|
| Trajectory Distribution | Shared by all users | Unique per person |
| Detection Difficulty | One cluster identifies all | Requires per-person modeling |
| Model Size | N/A | ~2MB |
The core shift: instead of using more complex rules to simulate “human”, let the model learn “you” from your own data.
Data Collection: Record Your Habits Non-Invasively
To do behavior cloning, first you need your personal mouse trajectory data.
The implementation is simple — a Tampermonkey userscript that listens to mousemove events and records the complete trajectory from mouse movement to click. Movements shorter than 20px are treated as stationary clicks and discarded. We care about movement patterns, not the click itself.
The data format is straightforward:
{
"viewport": {"w": 2018, "h": 1075},
"trajectory": [
{"x": 1480, "y": 322, "t": 0},
{"x": 1504, "y": 317, "t": 31},
{"x": 1501, "y": 319, "t": 69}
],
"target": {"tag": "DIV", "text": "Code"}
}
x/y are viewport coordinates, t is the time offset in milliseconds relative to the trajectory start. The target HTML tag is included because clicking a button vs. clicking a link produces genuinely different trajectories — buttons have larger target areas and more casual movements; links have smaller targets with more cautious end phases.
After a few days of normal browsing, you’ll have hundreds to thousands of trajectories. Then use the Tampermonkey menu to export a .jsonl file and drop it into the project’s data/ directory.
Architecture: Three Models, Each Handles One Thing
With the data collected, the initial approach was to train a large GRU model for end-to-end spatial and temporal prediction. But experiments showed this didn’t work well — the model would either learn overly smooth spatial trajectories (losing personalized arc styles) or uniform timing (losing acceleration/deceleration rhythms).
The solution was to decompose the problem into three modules:
Bezier (Skeleton) → NoiseModel (Spatial Deviation) → GRU (Timing)
-
Bezier Curve generates the macroscopic skeleton — it’s a fixed algorithm, doesn’t learn anything, just guarantees a reasonable path from start to end.
-
NoiseModel is a small GRU (~166KB) that receives Bezier control points and outputs your personalized (x, y) path. It learns how you deviate from the ideal path — what arcs you prefer, how much you jitter.
-
GRU (~2MB) takes the NoiseModel’s spatial path and predicts only when each point is reached — where you speed up, slow down, or hesitate.
Decomposing this way worked much better. Intuitively, the arc you take and when you accelerate/decelerate are two separate things. Learning them independently keeps each model cleaner.
How NoiseModel Learns Spatial Deviation
NoiseModel’s input is 8 Bezier curve parameters (start point, end point, two control point coordinates, normalized to viewport). It autoregressively generates a series of (x, y) points.
During training, the first 5 epochs use pure teacher forcing with real trajectory points, then gradually reduce until the model fully self-predicts. This way it learns real data patterns while being able to generate paths independently at inference time.
One detail: points in the final 20% of the trajectory get 4x loss penalty during training. The deceleration and fine-tuning phase near the target is where human vs. machine differences are most pronounced — machines tend to arrive straight-on, while humans show subtle overshoot and correction.
How GRU Learns Timing
GRU’s input is per-step “relative space” features: distance remaining to target, how far the last step moved, how long the last step took, current progress. Not absolute coordinates — because when humans move a mouse, the brain processes “the target is still that direction, roughly how far away”, not “the cursor is at pixel coordinates on screen”.
There’s a gotcha with time handling: in the raw data, time differences between adjacent points range from 8ms to 3494ms (extreme outliers). Direct training would be dominated by these outliers. The fix is a log1p transform — compressing the range to [0, 8.8], then using expm1 inverse transform after training.
Another practical finding: the model tends to predict slower times than actual. Adding a 0.70 scaling factor after training makes the generated total duration match the real median.
Why Not Transformer
You might ask, why GRU when everyone’s using Transformers now? Three reasons:
- Trajectories are strongly continuous unidirectional sequences — GRU’s inductive bias naturally matches this
- Inference requires autoregressive generation, each step depends only on the previous one — GRU is much lighter than Transformer
- Small dataset (hundreds of samples) — GRU generalizes better on small data
Training: Are a Few Hundred Samples Enough?
Honest answer: a few hundred is barely enough. The ideal is thousands per person.
In practice, both models are trained entirely on real data, no synthetic data dependency. The training strategy is to upweight the few hundred real samples (default ×10), telling the optimizer “prioritize fitting these real samples first”.
Training speed is also decent: GRU single epoch on GPU is about 45 seconds, 200 epochs under 3 minutes. CPU works too, just takes a bit longer.
Final output: NoiseModel ~166KB, GRU ~2MB. Two files totaling under 3MB, CPU inference latency under 5ms.
An Overlooked Detail: Event Rate
GRU-generated trajectories typically have only ~30 points, with ~27ms intervals — roughly 37Hz sampling rate.
But what’s the real mouse sampling rate? Browser-captured mousemove is usually around 60Hz (~16ms). Actual mouse hardware sampling rate is typically 125Hz (~8ms).
30 points looks too sparse. More critically: if your automation always produces exactly 30 points, that’s an obvious artificial fingerprint. Real human mouse movements have variable event counts — a 700px movement might generate 20 effective events when moving fast, or 80 when moving slowly with fine adjustments.
So a resampling layer is added after GRU output — “translating” the model’s predicted timing contour into real mouse event rates:
- Adaptive intervals: sparse during fast phases (~14ms), dense during deceleration (~4ms)
- Plus-minus 3ms time jitter: simulates hardware sampling noise
- Plus-minus 0.3px spatial jitter: simulates sensor accuracy
- Variable point count: same start/end generates 20-80+ events each time
This step only does “translation”, it doesn’t change the model’s predicted velocity curve. When the model says “this action takes 800ms, accelerates in the middle, decelerates at the end”, resampling just breaks that into irregularly-spaced events.
Results: How Does It Look
Let’s look at the comparison directly.
Left: three-stage pipeline generated trajectory (Bezier skeleton + NoiseModel spatial deviation + GRU timing). Right: pure Bezier curves. The pipeline trajectory isn’t a smooth mathematical curve — it has personalized arc deviations, uneven speed, and subtle hesitation near the end.
The dynamic comparison is more intuitive:
- Mechanical (gray): instant teleport to target, pure algorithm
- Bezier (blue): smooth uniform glide, still algorithmic
- GRU Model (gold): acceleration, hesitation, end-phase micro-adjustment — style learned from real data
Quick Start
# 1. Install dependencies
pip install torch numpy matplotlib
# 2. Place your exported .jsonl files in the data/ directory
# 3. Train NoiseModel (spatial personalization)
python training/generate_trajectories.py --train-noise --epochs 100
# 4. Train GRU (timing personalization)
python training/train_mouse_model.py --epochs 200
# 5. Visualize results
python examples/demo.py --save output.png
python examples/animate_demo.py --save output.gif
Integrate into browser automation:
from mouse_controller import move_to_humanized
# Before clicking
await move_to_humanized(page, target_x, target_y, tag="BUTTON")
# Then execute click
await page.click(target_x, target_y)
If the model fails to load, the framework automatically falls back to Bezier curves — no breaking changes.
Limitations
A few limitations worth being transparent about:
Data volume. A few hundred samples is barely sufficient; the ideal is thousands per person. The good news is this is a positive flywheel — more data means a better clone of you.
NoiseModel’s assumption. The current “real = Bezier + residual” assumption is strong. A better approach would use a generative model (diffusion or VAE) to generate full trajectories directly.
Multimodal. Mouse trajectory is just one dimension of behavioral fingerprinting. Keyboard rhythm, scroll patterns, mouse dwell time aren’t modeled yet. For keyboard rhythm, add a keyboard.jsonl under data/ (same format as trajectory) and adapt the scripts under training/ to reuse the existing GRU pipeline.
These limitations are also directions for future iteration.
Closing Thoughts
The AI browser agent ecosystem in 2026 is maturing rapidly. Operation accuracy is no longer the core bottleneck. The next bottleneck is trust — platforms don’t trust AI traffic, users don’t trust mechanical operations.
The future of AI operating computers shouldn’t be “a generic robot simulating a generic human” — it should be your AI assistant, operating in your style, with your decision preferences, and behavioral characteristics that match you.
All code and data in this project is for technical research and personalized AI research use only.
Project code: https://github.com/YuBing-link/mouse-behavioral-clone
Paper reference: FP-Agent: Fingerprinting AI Browsing Agents, arXiv:2605.01247, May 2026
How I Built a Vedic Panchang Engine in TypeScript — Swiss Ephemeris, Meeus Fallback, Zero External APIs
Sub-arcsecond planetary positions, sunrise for any location on Earth, and an entire astronomical calendar — all computed server-side in a Next.js app.
For the past year I’ve been building Dekho Panchang — an Indian Vedic astrology web app that computes a daily Panchang (the traditional Hindu almanac) for any location on Earth. Every astronomical value — sunrise, sunset, Moon’s position, tithis, nakshatras, yogas, karanas — is computed locally on the server. No external ephemeris APIs. No third-party astrology services.
The production engine uses Swiss Ephemeris (sub-arcsecond accuracy, the same library used by professional observatories) with a pure-math Meeus fallback for environments where native modules can’t load. This post walks through the architecture and mathematics, with real code from the production codebase.
What Is a Panchang?
A Panchang (पञ्चाङ्ग, literally “five limbs”) is the daily Indian almanac. The five limbs are:
| Limb | What It Measures | Astronomical Basis |
|---|---|---|
| Tithi | Lunar day (1-30) | Moon–Sun elongation ÷ 12° |
| Nakshatra | Lunar mansion (1-27) | Moon’s sidereal longitude ÷ 13°20′ |
| Yoga | Sun-Moon combination (1-27) | (Sun + Moon sidereal) ÷ 13°20′ |
| Karana | Half a tithi (1-11 types) | Moon–Sun elongation ÷ 6° |
| Vara | Weekday | Julian Day → day of week |
Every Hindu festival date, auspicious muhurta (electional window), and fast (vrat) is determined by these five values. Get the Sun or Moon wrong by half a degree, and a festival lands on the wrong day.
The Dual-Engine Architecture
The core design decision: every astronomical function checks for Swiss Ephemeris first, falls back to Meeus if unavailable.
export function sunLongitude(jd: number): number {
if (isSwissEphAvailable()) {
return swissPlanetLongitude(jd, 0).longitude; // < 0.001° accuracy
}
return _meeusSunLongitude(jd); // ±0.01° fallback
}
export function moonLongitude(jd: number): number {
if (isSwissEphAvailable()) {
return swissPlanetLongitude(jd, 1).longitude; // < 0.001° accuracy
}
return _meeusMoonLongitude(jd); // ±0.5° fallback
}
Swiss Ephemeris loads as a native Node.js module on the server — it’s the sweph npm package wrapping the C library from the Astronomical Institute at the University of Zurich. On the client side or in edge runtimes where native modules can’t load, Meeus kicks in automatically.
| Engine | Sun | Moon | Outer Planets | Where It Runs |
|---|---|---|---|---|
| Swiss Ephemeris | ±0.001° | ±0.001° | ±0.001° | Server (Node.js) — primary |
| Meeus | ±0.01° | ±0.5° | ±1-3° | Browser/Edge — fallback |
The Swiss Ephemeris integration includes a memoisation cache (keyed on JD rounded to 6 decimals) to avoid redundant calls — a single panchang computation queries the same JD dozens of times for different planets:
const planetCache = new Map<string, PlanetResult>();
const CACHE_MAX = 500;
function cacheKey(jd: number, planetId?: number): string {
return `${jd.toFixed(6)}${planetId !== undefined ? `:${planetId}` : ''}`;
}
Step 1: Julian Day — The Universal Time Counter
Both engines start by converting a calendar date to a Julian Day Number — a continuous day count used by astronomers since 1583. JD 2451545.0 = 1 January 2000, 12:00 UT (the J2000.0 epoch).
The algorithm comes from Jean Meeus’s Astronomical Algorithms (Ch.7):
export function dateToJD(
year: number, month: number, day: number, hour: number = 0
): number {
// Meeus convention: Jan/Feb treated as months 13/14 of prior year
if (month <= 2) { year -= 1; month += 12; }
// Gregorian correction for the 10-day gap
const A = Math.floor(year / 100);
const B = 2 - A + Math.floor(A / 4);
return Math.floor(365.25 * (year + 4716))
+ Math.floor(30.6001 * (month + 1))
+ day + hour / 24 + B - 1524.5;
}
Everything downstream — Sun position, Moon position, sunrise — takes a JD as input. Swiss Ephemeris has its own swe_julday function, but ours matches it to 10 decimal places.
Step 2: The Meeus Fallback — How It Works
When Swiss Ephemeris isn’t available, the Meeus algorithms handle everything. Here’s the Sun’s position via Meeus Ch.25:
function _meeusSunLongitude(jd: number): number {
const t = (jd - 2451545.0) / 36525.0; // Julian centuries from J2000.0
// Geometric mean longitude
const L0 = normalizeDeg(280.46646 + 36000.76983 * t + 0.0003032 * t * t);
// Mean anomaly — angle from perihelion
const M = normalizeDeg(357.52911 + 35999.05029 * t - 0.0001537 * t * t);
const Mrad = M * Math.PI / 180;
// Equation of centre — correction for elliptical orbit
const C = (1.914602 - 0.004817 * t) * Math.sin(Mrad)
+ (0.019993 - 0.000101 * t) * Math.sin(2 * Mrad)
+ 0.000289 * Math.sin(3 * Mrad);
const sunTrue = normalizeDeg(L0 + C);
// Nutation + aberration correction
const omega = 125.04 - 1934.136 * t;
return normalizeDeg(sunTrue - 0.00569
- 0.00478 * Math.sin(omega * Math.PI / 180));
}
The dominant term (1.9146°) in the equation of centre reflects Earth’s orbital eccentricity (~0.017). The Sun moves ~1°/day, so ±0.01° accuracy means we’re within ~14 seconds of the true position.
The Moon is more complex — Meeus Ch.47 implements a truncated ELP-2000/82 lunar theory with 60 periodic terms:
function _meeusMoonLongitude(jd: number): number {
const t = (jd - 2451545.0) / 36525.0;
// Four fundamental arguments (degrees)
const Lp = normalizeDeg(218.3164477 + 481267.88123421 * t
- 0.0015786 * t * t + t**3 / 538841);
const D = normalizeDeg(297.8501921 + 445267.1114034 * t
- 0.0018819 * t * t + t**3 / 545868);
const M = normalizeDeg(357.5291092 + 35999.0502909 * t
- 0.0001536 * t * t);
const Mp = normalizeDeg(134.9633964 + 477198.8675055 * t
+ 0.0087414 * t * t + t**3 / 69699);
const F = normalizeDeg(93.2720950 + 483202.0175233 * t
- 0.0036539 * t * t);
// Sum 60 periodic terms: each is sin(aD + bM + cMp + dF) × coefficient
let sumL = 0;
for (const [coeffD, coeffM, coeffMp, coeffF, sinCoeff] of MOON_TERMS) {
const arg = coeffD * D + coeffM * M + coeffMp * Mp + coeffF * F;
sumL += sinCoeff * Math.sin(arg * Math.PI / 180);
}
// Planetary corrections (Venus, Jupiter perturbations)
const A1 = normalizeDeg(119.75 + 131.849 * t);
const A2 = normalizeDeg(53.09 + 479264.290 * t);
sumL += 3958 * Math.sin(A1 * Math.PI / 180)
+ 1962 * Math.sin((Lp - F) * Math.PI / 180)
+ 318 * Math.sin(A2 * Math.PI / 180);
return normalizeDeg(Lp + sumL / 1_000_000);
}
The Moon’s orbit is perturbed by the Sun, Jupiter, and the non-spherical Earth. Even this 60-term truncation gives ±0.5° — well within the 13°20′ span of each nakshatra. In production, Swiss Ephemeris handles the Moon with full precision.
Step 3: Tropical → Sidereal (The Ayanamsha Bridge)
Western astronomy uses the tropical zodiac (anchored to the vernal equinox). Indian astronomy uses the sidereal zodiac (anchored to fixed stars). The difference is called ayanamsha — the accumulated precession of the equinoxes.
Swiss Ephemeris has built-in ayanamsha computation for multiple systems. Our fallback uses the Lahiri (Chitrapaksha) polynomial, anchoring the star Spica (Chitra) at exactly 180° sidereal:
export function lahiriAyanamsha(jd: number): number {
if (isSwissEphAvailable()) {
return swissAyanamsha(jd); // Uses SE_SIDM_LAHIRI mode
}
const t = (jd - 2451545.0) / 36525.0;
// ~50.29 arcseconds/year precession rate
return 23.85306 + 1.39722 * t + 0.00018 * t * t;
}
function toSidereal(tropicalLong: number, jd: number): number {
return normalizeDeg(tropicalLong - lahiriAyanamsha(jd));
}
In 2026, the ayanamsha is approximately 24.2° — meaning the tropical and sidereal zodiacs differ by nearly a full sign. This is why your “Sun sign” in Western astrology is often one sign ahead of your Vedic rashi.
Step 4: Computing the Five Limbs
With Sun and Moon positions in sidereal coordinates, the five Panchang elements are straightforward:
// Tithi: 30 lunar days per synodic month
// Each tithi = 12° of Moon-Sun elongation
function calculateTithi(jd: number): { number: number; degree: number } {
const sunSid = toSidereal(sunLongitude(jd), jd);
const moonSid = toSidereal(moonLongitude(jd), jd);
const diff = normalizeDeg(moonSid - sunSid); // 0° = new moon, 180° = full
return { number: Math.floor(diff / 12) + 1, degree: diff };
}
// Nakshatra: Moon's sidereal position in 27 equal divisions
function getNakshatraNumber(moonSidereal: number): number {
return Math.floor(moonSidereal / (360 / 27)) + 1; // 1-27
}
// Yoga: sum of Sun + Moon sidereal longitudes, divided by 13°20'
function calculateYoga(jd: number): number {
const sunSid = toSidereal(sunLongitude(jd), jd);
const moonSid = toSidereal(moonLongitude(jd), jd);
const sum = normalizeDeg(sunSid + moonSid);
return Math.floor(sum / (360 / 27)) + 1; // 1-27
}
// Karana: half a tithi — each 6° arc of Moon-Sun elongation
// 7 repeating types cycle through 56 slots, 4 fixed types fill the edges
function calculateKarana(jd: number): number {
const { degree } = calculateTithi(jd);
const idx = Math.floor(degree / 6);
if (idx === 0) return 11; // Kimstughna
if (idx >= 57) return [8, 9, 10][idx - 57]; // Shakuni, Chatushpada, Naga
return ((idx - 1) % 7) + 1; // Bava through Vishti cycle
}
These functions are engine-agnostic — they call sunLongitude() and moonLongitude(), which internally route to Swiss Ephemeris or Meeus. The panchang logic itself doesn’t know or care which engine produced the positions.
Step 5: Sunrise — The Panchang Day Boundary
In Vedic tradition, the panchang day runs sunrise to sunrise (not midnight to midnight). All five limbs are evaluated at the moment of sunrise. Swiss Ephemeris provides precise sunrise via atmospheric refraction models, but the Meeus fallback solves the hour angle equation directly (Meeus Ch.15):
cos(H) = (sin(-0.8333°) - sin(lat) × sin(δ)) / (cos(lat) × cos(δ))
Where -0.8333° accounts for atmospheric refraction (34 arcminutes) and the Sun’s semi-diameter (16 arcminutes). Then sunrise = solar noon − H/15 hours.
function approximateSunrise(jd: number, lat: number, lng: number): number | null {
if (isSwissEphAvailable()) {
return swissSunrise(jd, lat, lng); // ±10 seconds accuracy
}
// Meeus fallback: ±2 minutes accuracy
const T = (jd - 2451545.0) / 36525;
const obliquity = 23.4393 - 0.0130 * T;
const sunLong = _meeusSunLongitude(jd);
const decl = toDeg(Math.asin(
Math.sin(toRad(obliquity)) * Math.sin(toRad(sunLong))
));
const cosH = (Math.sin(toRad(-0.8333)) - Math.sin(toRad(lat)) * Math.sin(toRad(decl)))
/ (Math.cos(toRad(lat)) * Math.cos(toRad(decl)));
if (cosH > 1 || cosH < -1) return null; // Polar — no sunrise
const H = toDeg(Math.acos(cosH));
// Equation of Time: corrects for orbital eccentricity + obliquity
const y2 = Math.tan(toRad(obliquity / 2)) ** 2;
const L0 = toRad(280.46646 + 36000.76983 * T);
const M = toRad(357.52911 + 35999.05029 * T);
const e = 0.016708634 - 0.000042037 * T;
const eot = toDeg(
y2 * Math.sin(2 * L0) - 2 * e * Math.sin(M)
+ 4 * e * y2 * Math.sin(M) * Math.cos(2 * L0)
- 0.5 * y2 * y2 * Math.sin(4 * L0)
- 1.25 * e * e * Math.sin(2 * M)
) * 4; // minutes
const solarNoon = (720 - 4 * lng - eot) / 60;
return ((solarNoon - H / 15) % 24 + 24) % 24; // UT hours
}
The Equation of Time (up to ±16 minutes) corrects for two effects: Earth’s orbital eccentricity makes the Sun appear to speed up and slow down, and the obliquity of the ecliptic means equal ecliptic arcs don’t correspond to equal time intervals.
How Accurate Is It?
We run automated regression tests on every commit for Bern (UTC+1/+2), Delhi (UTC+5:30), and Seattle (UTC-7/-8) — covering DST transitions, half-hour offsets, and western hemisphere edge cases.
| Element | Swiss Ephemeris (production) | Meeus Fallback |
|---|---|---|
| Sunrise/Sunset | ±10 seconds | ±2 minutes |
| Moonrise/Moonset | ±30 seconds | ±5 minutes |
| Tithi transition | ±1 second | ±1 minute |
| Nakshatra transition | ±1 second | ±1 minute |
| Yoga / Karana | Exact | Exact (within element span) |
| Planet longitudes | < 1 arcsecond | Sun ±36″, Moon ±30′ |
// From our actual test suite
it('sunrise within 2 min of reference', () =>
assertWithinMinutes('sunrise', computed.sunrise, reference.sunrise, 2));
it('tithi end time within 2 min', () =>
assertWithinMinutes('tithi end', computed.tithiEnd, reference.tithiEnd, 2));
Hard Lessons from Production
Building this over the past year, I’ve catalogued 63+ bugs across 6 audit rounds. Some highlights:
The Timezone Trap
// BAD: Creates date in server's local timezone (UTC on Vercel, UTC+2 on my laptop)
const d = new Date(1990, 2, 15, 10, 30);
// GOOD: Always use UTC explicitly
const d = new Date(Date.UTC(1990, 2, 15, 10, 30));
Every single new Date(year, month, day) call in computation code is a landmine. On Vercel (UTC), it works fine. On a dev machine in Switzerland (UTC+2), birth time 10:30 IST silently becomes 10:30 CET — and your dasha dates drift by 5.5 hours.
The Sidereal Double-Subtraction
Moon’s longitude from Swiss Ephemeris is already sidereal (ayanamsha pre-subtracted). One module re-derived the nakshatra by subtracting ayanamsha again. Result: Moon appeared to be in the wrong nakshatra — off by nearly a full mansion. The fix: always use the stored .nakshatra.id or .sign instead of re-deriving from longitude.
Midnight-Crossing Time Ranges
Rahu Kaal might run 23:30–01:15. A naïve now >= start && now < end check never triggers because start > end. The fix:
function isTimeActive(now: number, start: number, end: number): boolean {
if (end < start) return now >= start || now < end; // Wraps past midnight
return now >= start && now < end;
}
This same bug appeared in three separate components before we found all instances.
Fixed Intervals for Lunar Phenomena
// WRONG: Moon's orbit is elliptical — intervals vary 13.9 to 15.6 days
const fullMoonDate = newMoonDate + 15 * DAY_MS;
// RIGHT: Binary search for the actual Moon-Sun elongation = 180°
function findFullMoon(startJd: number): number {
// Newton-Raphson or bisection on moonSid - sunSid = 180°
}
We shipped Purnimant month boundaries that used a fixed 15-day offset from New Moon. Result: months started on Ashtami instead of Purnima. The Moon’s elliptical orbit means this interval varies by nearly 2 days.
The Stack
- Framework: Next.js 16 (App Router, React 19, TypeScript)
-
Primary engine: Swiss Ephemeris via
swephnpm package (sub-arcsecond, 5400 BC – 5400 AD) - Fallback engine: ~3,000 lines of Meeus algorithms (pure TypeScript, no native deps)
- Deployment: Vercel with ISR (revalidate every 24 hours)
- Testing: Vitest with automated regression across 3 timezone locations
The entire panchang calculation runs in ~15ms on a cold start. No network requests, no API keys, no rate limits.
Try It
dekhopanchang.com — free, no signup needed. Enter any location and get the complete Panchang with all five limbs, sunrise/sunset, Rahu Kaal, auspicious windows, and more.
The full computation engine handles:
- Daily Panchang for any location globally
- Birth charts (Kundali) with 9 planets, 12 houses, and interpretive commentary
- Dasha systems (Vimshottari, Yogini, Chara, and 5 more)
- 80+ yoga detections with classical source references
- Festival calendar with Amanta/Purnimanta month support
- Muhurta (electional astrology) with multi-factor scoring
- Compatibility matching (Ashta Kuta, 36 points)
All computed from first principles. No black boxes.
If you’re interested in astronomical computing, celestial mechanics, or the intersection of ancient knowledge systems and modern engineering — I’d love to hear your thoughts in the comments.
The codebase handles 142 pages across multiple locales, serves users from Seattle to Delhi to Bern, and every number on every page is computed fresh from orbital equations. It’s the most fun I’ve had with trigonometry since university.
`
etcd: mvcc: database space exceeded: full recovery guide for on-prem Kubernetes
It was a regular working day when the first alert landed. Kubernetes health check showing the control plane was degraded. I’d seen these before. Usually a quick look, a quick fix.
Then I ran kubectl get nodes. The command just hung. I ran it again and this time it returned, slowly. Something was off but not obviously broken. Over the next few minutes, kubectl became increasingly unreliable. Commands that worked on one attempt would hang on the next. Then the errors started appearing consistently: TLS handshake timeouts, EOF errors. Eventually, kubectl stopped responding altogether
This was a production-grade, on-prem Kubernetes cluster. Three master nodes, high availability setup, the kind of architecture you build specifically so that one node going down doesn’t take everything with it. But right now, all three masters were effectively unreachable from the outside world. The API server was down. kubectl is my primary tool for everything Kubernetes was completely useless.
I didn’t know what was wrong yet. What I did know was that something had gone very, very wrong at a layer deeper than I usually have to look.
What Was Actually Breaking
The error messages were misleading. When kubectl did respond at all, it threw:
Unable to connect to the server: net/http: TLS handshake timeout
or
Unable to connect to the server: EOF
On the surface, this looks like a network problem. Or a certificate issue. Or maybe the API server itself had crashed. I went through the checklist: network connectivity between nodes was fine, certificates hadn’t expired, CPU and memory on the nodes looked normal.
Then I looked at the etcd container logs.
That’s when I saw it: NOSPACE.
The etcd container was restarting every few seconds. In the logs between restarts, the same alarm repeated: the database had exceeded its storage limit and etcd had frozen all writes. No writes meant the API server couldn’t record any state changes, couldn’t serve requests, couldn’t function.
I checked the disk usage on all three master nodes:
du -sh /var/lib/etcd
- Master node 1: 1.1G
- Master node 2: 2.1G
- Master node 3: 2.4G
Three nodes in the same cluster, with databases that were wildly different sizes. That asymmetry alone told a story these databases had never been compacted, never been defragmented. They had just grown, revision by revision, quietly, until the biggest one hit the limit and took the whole control plane down with it.
Why This Happens: etcd Compaction Explained Simply
etcd is the brain of a Kubernetes cluster. Every resource you create, every label you add, every pod that starts or stops all of it gets written to etcd. It’s the source of truth for the entire cluster state.
Every change creates a new revision a numbered snapshot of what the cluster state looked like at that moment. etcd keeps every revision, forever, unless you explicitly tell it to clean up. This design is intentional; it enables features like watch notifications and rollback. But it means that in a busy cluster with no maintenance configured, the database grows continuously.
Compaction is the process of telling etcd: “You can forget everything before revision #X. Those old snapshots are no longer needed.”
Compaction removes the historical revision records, freeing up logical space in the database.
But compaction alone isn’t enough to actually reduce disk usage. After compaction marks old records as deleted, etcd’s database file still occupies the same size on disk. It just has empty pages where the deleted records used to be. That’s where defragmentation comes in. Defrag physically rewrites the database file, reclaiming the space that compaction made available.
Two separate operations, both required. Most runbooks only mention one.
When the database hits its size limit (default: 2GB), etcd raises the NOSPACE alarm and stops accepting any writes. The API server tries to write state, can’t, fails, restarts. kubectl sends requests to the API server, gets no response, times out. The whole control plane seizes up.
Why didn’t --auto-compaction-retention save us? Because it wasn’t configured. In many kubeadm clusters, automatic compaction is not enabled by default. Nobody added it during cluster setup. Nobody noticed the database growing. Until the day it didn’t.
The Dead End: Why the Standard Fix Didn’t Work
Every Stack Overflow answer for etcd NOSPACE starts with the same step: kubectl exec -it etcd-<node-name> -n kube-system -- sh. I couldn’t do that.
kubectl was dead. The API server was down. kubectl exec requires the API server to route the request to the container runtime on the node. Without it, the command goes nowhere. I was stuck outside a burning building without a key, and every guide was telling me to use the front door.
I tried everything in the “normal” playbook. kubectl get pods -n kube-system timeout. kubectl describe pod etcd-master-1 -n kube-system timeout. Every kubectl command ended the same way.
Then I remembered etcd is a static pod. Static pods are different from regular pods. Regular pods are scheduled by the Kubernetes scheduler, tracked by the API server, managed through the control plane. Static pods are defined by YAML manifests placed directly on the node at /etc/kubernetes/manifests/, and they’re started directly by kubelet the agent running on each node without any involvement from the API server.
This means etcd doesn’t need the API server to run. It was running right now, on the nodes, restarting every few seconds, completely independent of the broken control plane above it. And if etcd is running directly on the node, I can access it directly on the node without kubectl, without the API server, without any of the Kubernetes abstraction layer.
That’s what crictl is for.
crictl is a command line tool that talks directly to the container runtime (containerd, in this case) on a node. It bypasses the entire Kubernetes API. If a container is running on a node, crictl can see it, exec into it, and interact with it regardless of whether the Kubernetes control plane is healthy or dead.
The door was never the front door. It was SSH.
The Fix
Step 1: SSH into a master node and find the etcd container
crictl ps | grep etcd

You’ll see the etcd container ID, its age (a few seconds if it’s crash-looping), and restart count. Note the container ID — you need it for the next step.
Step 2: Get a shell inside the etcd container
crictl exec -it 00565a01311ed sh
Replace 00565a01311ed with the actual container ID from your output. This drops you into a shell inside the running etcd container. No kubectl needed.
Step 3: Check the current revision and confirm the alarm
etcdctl --endpoints=https://127.0.0.1:2379
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
--cacert=/etc/kubernetes/pki/etcd/ca.crt
endpoint status -w json
A few notes on these flags: --endpoints points to the local etcd member; --cert and --key are the server certificate and key for mTLS authentication; --cacert is the CA certificate that signed them. You need all three because etcd requires mutual TLS it won’t accept unauthenticated connections even from localhost.
The JSON output will show you the revision number and the NOSPACE error explicitly.

Note the revision value. That’s what you’ll compact to. Also notice dbSize (2.1GB, the actual file size) vs dbSizeInUse (829MB, the data actually needed). That gap over 1.2GB of wasted space is exactly what compaction + defrag will reclaim.
Step 4: Compact to the current revision
etcdctl --endpoints=https://127.0.0.1:2379
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
--cacert=/etc/kubernetes/pki/etcd/ca.crt
compact 98469458
This tells etcd: “Everything before revision 98469458 can be discarded.” All those historical snapshots of cluster state that accumulated over months was gone. The output confirms:
compacted revision 98469458
Step 5: Defrag to physically reclaim the space
etcdctl --endpoints=https://127.0.0.1:2379
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
--cacert=/etc/kubernetes/pki/etcd/ca.crt
defrag
Compaction marked the data as deleted. Defrag actually removes it. The database file gets rewritten. Output:
Finished defragmenting etcd member[https://127.0.0.1:2379]
Step 6: Disarm the NOSPACE alarm
etcdctl --endpoints=https://127.0.0.1:2379
--cert=/etc/kubernetes/pki/etcd/server.crt
--key=/etc/kubernetes/pki/etcd/server.key
--cacert=/etc/kubernetes/pki/etcd/ca.crt
alarm disarm
Step 7: Verify disk usage and repeat on all master nodes
du -sh /var/lib/etcd

From 2.4GB to 434MB. Then exit, SSH into master node 2, repeat the entire process. Then master node 3. Each etcd member has its own copy of the database. You have to defrag all of them.
Step 8: Watch the cluster come back
Once all three nodes are defragmented and alarms are disarmed, etcd becomes healthy again. As soon as etcd accepts writes, the API server reconnects and starts serving requests. Within a minute or two, kubectl get nodes returns output. The control plane is alive.
How to Never Let This Happen Again
Enable automatic compaction. Open /etc/kubernetes/manifests/etcd.yaml on each master node and add this flag to the etcd command:
- --auto-compaction-retention=1h
This tells etcd to automatically compact every hour, discarding revisions older than one hour. kubelet will restart etcd automatically when it detects the manifest change. No manual intervention required.
What I Learned
The obvious lesson is: configure auto-compaction. If I’d added --auto-compaction-retention=1h during cluster setup, this incident never happens.
But the deeper lesson is about the Kubernetes abstraction layer and when it fails you.
I spent the first fifteen minutes of this incident trying every kubectl variant I could think of, because that’s what you do. kubectl is how you interact with Kubernetes. When kubectl doesn’t work, the instinct is to assume you’re doing something wrong, or that there’s a certificate issue, or that kubectl itself is broken.
The right question was different: why is kubectl not working, and what still works when kubectl can’t?
The answer is that kubectl is just a client for the Kubernetes API. When the API is down, kubectl is useless but the infrastructure below the API is still running. Static pods still run. crictl still talks to the container runtime. SSH still works. The cluster’s own database is right there, accessible directly on the node.
crictl is not a tool most Kubernetes engineers reach for in normal operations. It’s a break glass tool for exactly this scenario: when the control plane is broken and you need to get to a container that the API server can’t help you reach. Every engineer who runs on-prem Kubernetes should know it exists and understand when to use it.
The runbook for etcd NOSPACE is well documented across the internet. What isn’t documented is what to do when the standard runbook assumes kubectl works and it doesn’t.
That’s the gap this incident sits in. And now you know how to cross it.
If this saved you during an incident, share it with the engineer who set up your cluster and forgot to add --auto-compaction-retention. They’ll thank you before this happens to them.
Why Does Google Keep Changing My Language? (And How to Fix It)
Have you ever searched for something on your browser and suddenly Google decides you now speak a completely different language just because of where you are? 😂
Maybe you searched for a programming tutorial and got results in French.
Maybe you traveled somewhere and your browser started showing local language websites you never asked for.
Or maybe Google saw your IP address and thought:
“Ah yes, this person definitely wants everything translated.”
Meanwhile you are just trying to search in peace. 😭
The good news is that there’s a permanent fix.
Instead of letting Google guess your language and region based on your location, you can customize the Google search engine URL so your browser always forces:
- English results
- English interface
- US-based search ranking
- Minimal personalization
- No annoying country redirects
And the best part?
It does NOT slow down your internet or increase latency.
The Permanent Fix
We are going to create a custom Google search engine URL.
Use this URL:
https://www.google.com/ncr/search?q=%s&hl=en&gl=us&lr=lang_en&pws=0
What each part means:
hl=en
Forces Google interface language to English.
gl=us
Makes Google prioritize US-style search results.
lr=lang_en
Prioritizes English-language pages only.
pws=0
Reduces personalized search results.
ncr
Means “No Country Redirect.”
Stops Google from redirecting you to country specific domains like:
- google.fr
- google.de
- google.co.ke
Step-by-Step Setup Guide
Chrome
Open:
chrome://settings/searchEngines
Click:
Add
Then enter:
Search Engine:
Google English
Shortcut:
g
URL:
https://www.google.com/ncr/search?q=%s&hl=en&gl=us&lr=lang_en&pws=0
Now click:
Save
Then click the 3 dots beside the new search engine and choose:
Make Default
Done ✅
Microsoft Edge
Open:
edge://settings/searchEngines
Click:
Add
Enter:
Search Engine:
Google English
Shortcut:
g
URL:
https://www.google.com/ncr/search?q=%s&hl=en&gl=us&lr=lang_en&pws=0
Save it and set it as default.
Done ✅
Firefox
Open:
about:preferences#search
Firefox handles this a little differently.
Create a new bookmark with this URL:
https://www.google.com/ncr/search?q=%s&hl=en&gl=us&lr=lang_en&pws=0
Then assign a keyword like:
g
Now whenever you type:
g your search here
Firefox will use your custom Google setup.
Done ✅
Bonus Tip: Disable Browser Location Access
Even after changing the search URL, some browsers still share your location.
You can disable that too.
Chrome / Edge
Open:
chrome://settings/content/location
or:
edge://settings/content/location
Then choose:
Don't allow sites to see your location
Done ✅
Final Result
After this setup:
- Google stops changing languages randomly
- Search results stay in English
- Country redirects disappear
- Results become more consistent
- Your browser behaves the same no matter where you travel
No VPN required.
No extensions required.
No slowdown.
Just clean, predictable Google searches.
About the Author
Written by Samuel Thuku, a software engineer who enjoys solving annoying everyday tech problems, automating things that should have worked properly in the first place, and making the internet a little less chaotic one fix at a time. 😄











