Posts in Devops (20 found)

Microsoft Patches a Record 570 Security Flaws

Microsoft Corp. today released software updates to plug at least 570 security holes in its Windows operating systems and other software, almost triple the number of vulnerabilities the software giant fixed in its record-smashing Patch Tuesday release last month. Microsoft attributed the burgeoning patch counts to vulnerability discoveries aided by artificial intelligence. Nearly 60 of the bugs quashed in July’s Patch Tuesday earned a “critical” severity rating, meaning miscreants or malware could use them to seize remote control over a Windows device with little or no help from the user. Microsoft also addressed three zero-day flaws, including two that are already being exploited in the wild. Two of the zero-day weaknesses allow an attacker to elevate their user rights on a Windows system, as do approximately 250 other elevation of privilege flaws fixed this month; they include CVE-2026-56155 — an Active Directory Federation Services bug — and CVE-2026-56164 , a Microsoft Sharepoint vulnerability. CVE-2026-50661 is a security feature bypass in Windows BitLocker that could allow attackers to gain access to encrypted data if they have physical access to the device. Microsoft said this bug has been detailed publicly, but that it is not aware of any active exploitation. In a blog post on July 9, Microsoft Executive Vice President Pavan Davuluri wrote that Windows users will notice “a higher volume of security updates included in each security release” as a result of AI aiding in the discovery of vulnerabilities. “The pace of vulnerability discovery is changing with advances in AI making it possible to find more issues, faster, across more code, with new mechanisms that can accelerate both discovery and analysis,” Davuluri wrote . Jack Bicer , director of vulnerability research at Action1 , called attention to CVE-2026-48561 , a remote code execution flaw in Microsoft Copilot (with a 9.6 CVSS threat score) that allows an unauthorized attacker to execute code over the network. Microsoft says an attacker could exploit this bug by hosting a malicious website that causes Microsoft Edge for Android to automatically send crafted prompts to Copilot when a user visits the site. As AI advances the state of vulnerability discovery and remediation, it is also making it easier for attackers to quickly devise working exploits for known software flaws. Microsoft has long labeled security bugs using its “exploitability index,” which is Redmond’s best guess as to how likely it is that attackers will be able to figure out a reliable way to exploit a given vulnerability. But Satnam Narang , senior staff research engineer at Tenable , argues that Microsoft’s exploitability index needs to do a better job of shifting with the machine speed of discovery. For example, Microsoft originally gave this month’s SharePoint zero-day an exploitability rating of “less likely,” although the flaw was added to CISA’s Known Exploited Vulnerabilities list on July 1. “Anthropic’s Red Team’s own findings for known vulnerabilities (n-days) revealed how fragile this system has become, with its Mythos Preview model being able to produce proof-of-concept exploits for 13 of 14 vulnerabilities that were rated ‘Exploitation Less Likely’ or ‘Exploitation Unlikely,'” Narang said. “What this means is that our way of looking at Patch Tuesday has changed, because the exploitability index is centered around humans, not AI tools, and as these tools continue to improve, defense needs to improve alongside it.” Chris Goettl at Ivanti observed that the record patch numbers from Microsoft come as a number of other major software makers are increasing their patch cadence, including Adobe which announced today it is moving to twice-monthly security bulletins published on the 2nd and 4th Tuesday of each month (Adobe also cited AI for accelerating their patch cycles). Cisco , Mozilla and Oracle also are shipping updates more frequently, while Google’s patch batches in June 2026 totaled more than 900 security fixes, Goettl noted. Backing up your Windows system and/or data is always a good idea before applying operating system updates. Given the volume of patches addressed this month it may be wise for end users to wait a few days before applying these fixes. It’s not uncommon for security patches to introduce system stability issues, and those chances probably increase quite a bit with the gigantic patch count released today. Further reading: Action1’s Patch Tuesday blog Automox’s rundown

0 views
Martin Fowler 2 days ago

DSLs Enable Reliable Use of LLMs

LLMs generate code incredibly fast, but to ensure they generate exactly what is intended, they need clear boundaries. Abstractions and Domain-Specific Languages (DSLs) provide a strong harness that guides LLMs right from the start. Unmesh Joshi describes how the example of Tickloom - a domain model and DSL for illustrating distributed system behavior - shows how we can use an LLM as a partner to iteratively build a DSL and as a natural language interface to use it. Such a DSL can act as the key source of truth for software systems in the world of LLMs.

0 views
ptrchm 2 days ago

Postgres Backups to S3 with WAL-G and Kamal

The Kamal setup guides I found online focus on S3 backups using . You don’t want that for a production database. A better solution is to set up your Postgres database for Point-In-Time Recovery (PITR) using WAL-G or pgBackRest. This means your database is continuously archiving WAL segments to an S3 bucket (roughly every 60 seconds), so you can restore to any point in time. With LLMs, it’s not that hard to set up. This quick guide focuses on WAL-G, because I’ve found it to be a lot easier to set up than pgBackRest.

0 views
./techtipsy 1 weeks ago

The 'free' server build

Earlier this year, I received an old PC for free from my teammate Kristjan Lepik. After confirming a few details, I understood that I could probably find an use case for it, especially given my unhealthy obsession with trying to make old hardware useful. Free tech tip: if a free PC has blue USB 3 ports visible, then it’s not completely obsolete yet! For most of its time, it sat idle. The components and case got shuffled around based on other project ideas and a desire to have a small gaming PC, but then it ended up back in its original configuration. One day, my trusty ThinkPad T430 that ran as a home server started encountering oddities around the network interface cutting out during periods of moderate to high load, and its “Power on with AC attach” feature was also flaking out on me, meaning that a prolonged power outage would result in a home server that doesn’t come back up again. That’s when I decided to make this free PC my home server. The machine initially came with these specs: Initially, the 8GB of RAM was quite limiting, but the board had three DIMM slots free, and by sheer luck, I found two different local listings for used Kingston memory, different revisions, but the same model and physical size! 30 EUR and a quick memtest later, I now had upgraded the machine to 32 GB of RAM. It’s DDR3 and I overpaid for it just to get a matching set of 4, but in this economy I’m more than happy with this arrangement. I moved over storage from my other builds. The OS lives on a 128 GB NVMe SSD, which is also bootable. The booting aspect is worth highlighting because when this motherboard was new, NVMe SSD-s in this form factor weren’t super common yet and booting from PCIe devices was not common. I also carried over two Samsung 870 QVO 4 TB SSD-s , and the two 18 TB white label Seagate drives. The motherboard does have six SATA ports, but with the NVMe SSD installed, only four of them are actually usable. Quirks and limitations like this are quite common on motherboards, so keep that in mind when planning your builds, especially with less capable hardware. Luckily this wasn’t a deal-breaker for me. After I caved and got a fancy gaming GPU for Forza Horizon 6 , I had an AMD Radeon RX 480 8GB model left over, the Sapphire Nitro version, which is arguably one of the best looking GPU-s out there. I plopped that in as it can still find use as a transcoding GPU, and it has been fun testing its capabilities with smaller language models that fit within its 8 GB of VRAM. Not bad for a GPU that’s almost exactly 10 years old, and I know that because the previous owner bought it in 2016 and had used it full-time since. Cooling is quite a limiting factor with this case, so I got a cheap Arctic case fan for it and set it to run in “Turbo” mode in UEFI settings. Dust will likely be an issue a year or two from now, but that’s better than overheating hardware. It doesn’t help with the GPU throttling under sustained loads, but at least the other components are fine. Thanks to running local language models, I got reminded of the power limit of my UPS. It’s 360 W, apparently. The case is just small enough so that I can put it on my infrastructure shelf. It hangs a bit over the edge, but it gets the job done. It’s noisy and the cheap and basic case makes hard drives audible, but given that it sits in a closet, I can’t hear it at all when the closet door is closed. Once I do open the door and the server is doing some heavy crunching, it does resemble the sound of a small data center. I’m aware that the bulk of the cost of this build is in the storage and GPU that I added on from previous builds, but I felt I needed to highlight the value of old equipment that someone else hasn’t used for years. Compute has become much less affordable, so going the second-hand route is all the more important and actually better for the environment, as long as the energy costs of operating the equipment isn’t too high. Ühe mehe vana on teise mehe uus. Intel i5-4690 with the stock cooler it’s a solid 4 core CPU with just enough performance for me this CPU is from 2014! ASUS H97M-PLUS it has an Intel gigabit network interface, which was a positive surprise for me! 8 GB of good ol’ DDR3 RAM NVIDIA GTX 750 Ti a basic Chieftec 500-ish W PSU, half-modular a basic PC case made of very thin metal

0 views
Giles's blog 1 weeks ago

poppy the training box, part 1: the beginnings

For a while I've been planning to put together a separate machine for local LLM training. Until now, I've been using my desktop PC, . I have an RTX 3090 installed, and can get useful training runs done (most recently, a 163M-parameter GPT-2 small style LLM in JAX ), but there are a couple of problems. And relatedly to all of those: the two-day limit to the training runs I've been doing is something I set because that's the maximum amount of time I'm willing to have tied up. It would be really interesting to try longer training runs! I also have longer-term plans; a multi-GPU box would be interesting to put together -- not just to have more power locally, but so that I could test larger-scale cloud multi-GPU training runs before starting to pay for expensive machines. US$15.92 an hour to rent a machine isn't a lot of money, but it adds up, especially if you're spending it while debugging parallelism issues. And finally, I've always been interested in putting together a custom water-cooling loop in a PC. I've been building my own machines since 1995 or so, but never got round to that side of things. It sounds fun! But despite all of those future plans, this is a fairly normal machine-building post -- how I repurposed an old PC, plugged in a second-hand RTX 3090 from eBay, tested it all, accidentally trained an LLM for 11 days, and almost cooked a CPU. Over time, I expect to be posting more -- and more interesting -- build details. Let's think of this as establishing the baseline. Back before I moved to Lisbon, we had a holiday home here. When we came over, I'd bring my laptop, but that was always somewhat unsatisfactory -- limited CPU power for work, limited GPU for my occasional gaming. During Covid, we started staying in the holiday home for longer periods -- and this became too big of an annoyance to ignore. So in 2020 I put together a small form-factor PC, which I named . The constraints were: The build was a bit fiddly, like all SFF PCs. You can see the component list and build notes here on PCPartPicker , but in short she had: She looked like this: (Gosh, I'd forgotten how... vivid our wallpaper was in that dining room.) For scale -- that case is slightly taller than two cans of coke stacked on top of each other. So, pretty small. When we moved to Lisbon full-time, I brought with me from London, and while he's been upgraded several times since (including adding an RTX 3090 in late 2023 ), he's been my daily driver since. So sat in the corner of my study, sad and unused :-( It was time to bring her out again. Initial plan: get her up and running in a new, larger case, with a PSU that could potentially handle three graphics cards. Initially, I found that she wouldn't switch on: a quick check suggested that the problem was the PSU. I'd had problems with SFF PSUs in the past, and given that the plan was to give her a new one, I just got one, along with a new, larger case -- specifically: A few days later, the parts arrived. Here's a family photo: is to the left, centre, sitting on top of her new case, and Cornélia (wearing her Flower of Shame) is to the right. For scale, Cornélia is quite a large cat. (I appreciate that that is not immensely helpful.) Time to put the old motherboard and the new PSU into the new case. Here's what it looked like: The Mini-ITX motherboard in a case designed for full ATX looks comically like a postage stamp. I switched her on, and luckily enough, everything worked! Must have been a PSU issue. The OS that she had was a more than three-year-old version of Arch, so I wiped the drives and installed the most recent version with my normal config, and it was time for a quick test. One of the nice things about having done all of this LLM training stuff recently is that you have a ready-made burn-in test for new hardware :-) I didn't have my JAX training code yet, but I did have the PyTorch one . Now, with her GTX 1660 Super GPU, was clearly not going to be able to train an LLM of the size I could with 's RTX 3090. I did some fiddling around with the model and training run parameters, and found that I could fit in a cut-down version of GPT-2 small with this setup: I trained it with a microbatch size of 4, gradient accumulation over 16 steps, and all other hyperparameters the same as my normal training runs on . The number of training tokens went down -- the model had 76,933,120 parameters, so I needed to train for just over 20x that -- about 1.5B instead of the 3.2B I've been training my other models on. I kicked that off, and out of interest, I kicked off another training run on with the same setup to see what happened. The training run went normally -- GPU running at full blast, 368W, and it completed in about 9 hours. That's less than 1/4 of the time my normal training runs take, which makes sense because time taken for this kind of thing scales roughly linearly with both the size of the model and the number of tokens, and both of those were about half the normal size. was a bit more interesting. In , the GPU usage showed up as 100%, but with an "effective" utilisation of 53%. The power draw matched the latter, being 67W out of a total possible 125W. I'm not quite sure what was causing that -- clearly there was a bottleneck somewhere. Not really worth digging into, though, given that I was going to replace the card shortly. Anyway, that took 963,257 seconds to run. That's 267.57 hours, or just over 11 days. What's kind of interesting there is that this training run not only took much longer (which is only to be expected), but that it used more electricity. 67W over 267.57 hours is just short of 18kWh, whereas 368W over 9 hours is about 3.3kWh. Buy an RTX 3090, save the planet! I decided to run my normal evals to confirm that what had come out the other end was sane. When asked to complete "Every effort moves you", 's model said: And 's said: Those were actually rather good, I thought! And looking at my normal loss test confirmed that the models really weren't that bad; 's got 3.855702, and 's 3.855981. That was actually better than the 3.943522 I'd got on before I went down my rabbit hole of optimising hyperparameters . So, that was an interesting test -- I was talking to ChatGPT about it at the time and it called it "maybe an art project", which I thought was amusing if a bit arch. Time to do something a bit more useful. Finding an RTX 3090 for a decent price from a trustworthy-seeming vendor is kind of hard right now. But it's still the sweet spot for price-performance if you're looking to train models locally, so I set up an alert on eBay, and eventually one popped up in Bulgaria. I bought it, and a few days later, this turned up: It's actually not as ugly as it looks in that photo -- it's considerably uglier. The stuff that looks a bit like crinkled aluminium foil is really white plastic with a kind of crystalline texture. Made me glad that I'd gone for the mesh-sided case rather than the glass one. Well, I hadn't bought it for the looks. I removed the old GTX 1660, and put in the new card, switched it on, and: Wow, a disco in my PC. Lovely. It was time to kick off another training run to see if it worked. This time, I did my normal GPT-2 small sized train with optimised hyperparameters. It ran for about ten minutes, and then switched herself off. That didn't look good. I spent some time digging around trying to work out why my new graphics card was broken, and then happened to be sending the video above to a friend, and spotted something. Check out the Noctua fan -- the beige and brown one you can see behind the cooler mount, above the graphics card. It wasn't spinning. That's the CPU cooler fan and should always be spinning, even if slowly, when the machine is on. I log basic metrics for all of my PCs to a central InfluxDB instance, so I checked that out and: A CPU temperature spike up to about 115°C! Not good. Clearly an emergency thermal shutdown from the CPU. I initially thought that I must have knocked the fan cable loose while plugging in the new GPU -- plausible, though they were quite far apart -- but unplugging then reseating it, then powering up the machine still didn't start the fan spinning. And it was not visible in the BIOS. I then zoomed out a bit in Grafana; I only keep 30 days' worth of metrics, and it had been more than a month since I did my original burn-in test, so I didn't have anything for that. But I did have this: had been idle for all of that time, and was averaging CPU temps of over 70°C. The dropoff prior to running the test was because she'd had a chance to cool down while I installed the GPU. Having spent ages setting up my InfluxDB monitoring stuff so that I have metrics for everything, I should probably actually look at them every now and then, because the fan had obviously not been doing anything for a month or so. Well, thank goodness for Amazon next-day delivery. I bought a new Noctua NF-A9x14 PWM (praying that the problem was the fan and not the header on the motherboard), and when it arrived, I put it in. This time, when I powered her on, the fan was spinning. Phew. I left her running for an hour, and the CPU temperature stabilised at 35.5°C. Next, I kicked off a version of my standard LLM training run with the number of tokens reduced so that it would run for an hour. During that, the CPU temperature went up to a moderately-toasty 76°C -- not ideal, but remember that with the broken fan, she was running that hot at idle. It seemed a bit odd that it was that hot at 10% CPU usage, but given that one core was running at 100%, it didn't seem totally off. The heatsink and fan are designed for SFF PCs anyway, and those tend to run somewhat hot. The GPU temperature also went up to 70°C and stabilised there, while power draw was stably about 368W out of 370W, and GPU utilisation at 100%. That was particularly pleasing because Nvidia cards throttle at 83°C or so by default, so if I was getting a lower temperature at full power, the fans clearly had some headroom for cooling. Once that was completed, it was time for another full training run for a burn-in. I kicked off my normal run. CPU and GPU temperatures stabilised at the same level as they had with the one-hour test, which was promising, so it was just a question of waiting... ...until I got this: About 40 hours, which is pretty much standard -- certainly the same as I'd expect from . The smoke test: Don't you just love it when your LLM tries to sell you something? 1 But anyway, loss on the test set was 3.548880, which is essentially the same as the same training run on too. So, now is a properly-configured training machine -- one RTX 3090, a CPU that runs a bit hot but at least doesn't do emergency shutdowns, and a case and a PSU with enough space for more GPUs. I think that the next step will be to move on to water cooling. In order to support more than one GPU, I'll need a new motherboard and probably a new CPU, so I don't think there's any point in watercooling the latter, despite its toastiness -- I'd just be buying a waterblock for it that I'd throw away in the not-too-distant future. Instead, I'll get the block for the GPU, and set up a loop to cool just that. Who knows -- maybe I can get rid of that horrendous RGB stuff at the same time! We live in hope. Also, that "expertise and expertise" tiny model smell.  ↩ is my daily driver. If he's doing a training run, then everything is just a little bit sluggish as CPU and GPU alike are busy. Although I don't play games often, it's annoying to have the option ruled out for days at a time. While the GPU is busy with a training run, I can't do other experiments in parallel -- for example, to scope out what the next step might be. Small enough to fit in a carry-on bag. I was building the machine in London, and wanted to be able to bring her to Portugal easily, and to be able to bring her back if I wanted to. Portable enough to quickly move around the flat. In the holiday home, the dining room was my study, so I wanted to be able to keep there normally, but move her when we had guests for dinner. Powerful enough to be able to run the games I was playing -- at the time I was a big fan of Assassin's Creed Odyssey , which didn't need a flagship card, but wasn't lightweight either. An AMD Ryzen 5 3600 3.6GHz 6-Core CPU A Noctua NH-L9a-AM4 CPU cooler A Gigabyte X570 I AORUS PRO WIFI Mini ITX Motherboard 32 GiB Corsair Vengeance DDR4 RAM 2x Samsung 970 Evo 500 GB NVMe SSDs A Zotac GTX 1660 Super 6 GiB GPU A Lian Li PC-TU100 Mini ITX case A Corsair SF450 450W SFF PSU An ASRock Phantom Gaming PG-1600G 1600W , which would have power in spades -- an RTX 3090 goes up to about 370W at full draw, so that should hopefully handle three of them plus a CPU without problems even if one or two of the GPUs had power spikes. A Fractal Design North XL . was already in a North (not the XL variant) and I love the case; the XL one looked like a good option if I was going to be cramming more GPUs in there, and had plenty of space for water-cooling. Vocab size: 50257 -- this was fixed because I was using the GPT-2 tokeniser. Context length: down from 1024 to 512 Embedding dimensions: down from 768 to 512 Number of heads: down from 12 to 8 Number of layers: down from 12 to 8 QKV bias: no (different to GPT-2, but the same as my own best local model). Also, that "expertise and expertise" tiny model smell.  ↩

0 views
Blog System/5 1 weeks ago

Autoconf’s revenge: ad-hoc shell templates

As powerful as Bazel is, sometimes it’s not featureful enough. When using this build system, it’s common practice to wrap it in a launcher script—and in fact, this is natively supported by Bazelisk , Bazel’s native dispatcher that stands for the binary in the user’s . Bazelisk will first download the version of Bazel requested by the project, and then, if exists, invoke it instead of the downloaded binary. is what’s known as a Bazel wrapper and is the point of today’s article. Well, not quite. The actual point of today’s article is to demonstrate a simple trick I learned from the GNU Autoconf and Automake days to implement full-blown conditionals in an ad-hoc template system. But because such trick is trivial once you see it, I have to present it in the context of a modern real-world scenario. So what I’m going to do is guide you through the creation of your very own Bazel wrapper to customize Bazel’s configuration file in ways that the native Bazel tool doesn’t support. Let’s get started. But wait! Take a moment to subscribe. I’m sure you’ll enjoy future posts, and it’s the only way for me to know that they are worth writing in the first place! Template systems are everywhere. Take any static blog generation system and you’ll find some. Take system management tools like Ansible and you’ll find others. Take a cloud orchestration service like Kubernetes and you’ll find Helm. Heck, even Go’s standard library provides a full blown text template system out of the box. There is clear benefit and appetite for these and, surely enough, it’s tempting to use any pre-existing such system in your own project… but if all you need are a bunch of variable replacements, some of which may be only conditionally applied, you can go a long way by not taking any dependencies. A call to or the substring function of your language of choice is all you need. To put this in context, let’s say you have Bazel’s configuration file, which is not very flexible, and that you need to set some arguments based on dynamic values that depend on the environment. E.g. something like this: If you know a little bit about , however, you may squint at that and say: “That’s silly! Make those flags conditional on a configuration and you’re set!” So you try something like this: And… this does not work. Gotcha! Startup flags cannot be placed behind a configuration so there is no way for you to parameterize the JVM’s max heap value passed in . And having to remember to pass from CI all the time is fragile, because you might forget and not get the desired configuration in place. Solving the above is not difficult if we could parameterize the configuration. We might want to write something like this instead: … and then have and be replaced dynamically depending on some runtime arbitrary logic. We can do that via the Bazel wrapper, and this sort of dynamic configuration is a common thing to do from it. So let’s do this. Let’s start with the template logic: Ugly(?) bash syntax but nothing too complicated: The global hashmap tracks variable names and their replacement values. The function inserts a new key/value pair into . (It’s important that the values given to don’t contain -special characters like , backslashes, or the separator we chose—but we control the generation of those values so we are good.) The function transforms the hashmap into a set of arguments of the form and then calls to process the given input file into the given output file. Then, we can plug everything together into a minimal Bazel wrapper: In this new snippet, the function instantiates the file from the contents of via and then calls the actual Bazel binary provided by Bazelisk in . The complexity here may seem overkill, but it’s necessary : while it’s pointless to invoke Bazel in parallel due to its global lock, users will run Bazel in parallel and you must make sure that the wrapper is reentrant. Otherwise, you’ll definitely run into races. The rest of the script in does the actual work to compute key/value pairs to substitute in your now-templated and then delegates to Bazel via . That’s it. This is a barebones implementation of a text template system using bash—and I had to use bash, not sh, to get the niceties of a hashmap —that serves as a launcher. Go try it. By the way, the and nomenclature are inherited from GNU Autoconf’s AC_SUBST primitive . “Great!” I hear you say in a sarcastic tone. “You have just applied string replacements! But what about conditionals, huh? You CaNnOt Do ThAt So EaSiLy!!11!one!” Ah, but you can , and showing you that trick is the whole point of this short article, remember? The necessary insight is that we can use string replacements to comment out lines in the original file. What if we did this: In here, we are defining different configurations for developer workstations and for CI, like we did earlier, but then we are auto-magically picking the default configuration depending on and . How? Well: will expand to the empty string when running on CI and will expand to , so the corresponding lines will be enabled and disabled. And the opposite replacement values will appear when not running on CI. Ta-da! Conditionals. We can make things nicer with a helper function and meta-programming: Don’t panic about that . Just as with the invocation above where we could have issues with special characters appearing in values, we control the arguments to so the is safe. And note that we can even nest conditionals arbitrarily. There is nothing preventing you from doing: Which corresponds to the conceptual equivalent of: Let’s do loops? Sorry no, can’t do! Well akshually… we could do loops. Not by using simple tricks like above, but we could definitely sketch something like this: However, this is starting to look a lot like a high-level parser, not scripting where you glue simpler components together. And if you are headed that way, you are better off transitioning to a proper programming language and a well-known template system. What do you think? Do you hate this already? You can, but note that the whole world runs on this stuff. All of that foundational code behind Linux systems ends up using GNU Automake and GNU Autoconf, and those packages are full of stuff like this in their and files. And you can get very far with just the above constructs if you treat the shell like a real language . The Bazel wrapper that I maintain at work these days grew to almost 1000 lines of code before I pruned a lot of features that had become unnecessary, but it’s still pretty large. We are now transitioning it to a Go-based wrapper for better readability and maintainability… but as we do this, I’m reminded that well-groomed shell scripts give you some flexibility that no other language can match in just a few lines. So, keep things simple. You can do a lot with just a few primitives. As powerful as Bazel is, sometimes it’s not featureful enough. When using this build system, it’s common practice to wrap it in a launcher script—and in fact, this is natively supported by Bazelisk , Bazel’s native dispatcher that stands for the binary in the user’s . Bazelisk will first download the version of Bazel requested by the project, and then, if exists, invoke it instead of the downloaded binary. is what’s known as a Bazel wrapper and is the point of today’s article. Well, not quite. The actual point of today’s article is to demonstrate a simple trick I learned from the GNU Autoconf and Automake days to implement full-blown conditionals in an ad-hoc template system. But because such trick is trivial once you see it, I have to present it in the context of a modern real-world scenario. So what I’m going to do is guide you through the creation of your very own Bazel wrapper to customize Bazel’s configuration file in ways that the native Bazel tool doesn’t support. Let’s get started. But wait! Take a moment to subscribe. I’m sure you’ll enjoy future posts, and it’s the only way for me to know that they are worth writing in the first place! The context Template systems are everywhere. Take any static blog generation system and you’ll find some. Take system management tools like Ansible and you’ll find others. Take a cloud orchestration service like Kubernetes and you’ll find Helm. Heck, even Go’s standard library provides a full blown text template system out of the box. There is clear benefit and appetite for these and, surely enough, it’s tempting to use any pre-existing such system in your own project… but if all you need are a bunch of variable replacements, some of which may be only conditionally applied, you can go a long way by not taking any dependencies. A call to or the substring function of your language of choice is all you need. To put this in context, let’s say you have Bazel’s configuration file, which is not very flexible, and that you need to set some arguments based on dynamic values that depend on the environment. E.g. something like this: If you know a little bit about , however, you may squint at that and say: “That’s silly! Make those flags conditional on a configuration and you’re set!” So you try something like this: And… this does not work. Gotcha! Startup flags cannot be placed behind a configuration so there is no way for you to parameterize the JVM’s max heap value passed in . And having to remember to pass from CI all the time is fragile, because you might forget and not get the desired configuration in place. Basic string replacements Solving the above is not difficult if we could parameterize the configuration. We might want to write something like this instead: … and then have and be replaced dynamically depending on some runtime arbitrary logic. We can do that via the Bazel wrapper, and this sort of dynamic configuration is a common thing to do from it. So let’s do this. Let’s start with the template logic: Ugly(?) bash syntax but nothing too complicated: The global hashmap tracks variable names and their replacement values. The function inserts a new key/value pair into . (It’s important that the values given to don’t contain -special characters like , backslashes, or the separator we chose—but we control the generation of those values so we are good.) The function transforms the hashmap into a set of arguments of the form and then calls to process the given input file into the given output file.

5 views

Syncing my clipboard between macOS and remote terminals

As I've been spending more and more time with agentic development, it's more and more important to me that sessions run somewhere other than my laptop. For the last few months, that has meant running my coding agents in tmux on either a remote Mac or a remote Linux server. The most frustrating thing for me has been that the clipboard or paste buffer on those remote hosts isn't synced to my desktop. So, if I copy something inside of a coding agent, I've had to play games to get it to my local Mac. Similarly, if I wanted to paste a screenshot to a remote tmux session. I was playing games with scp. But also, I don't just want to be able to copy and paste between my Mac and a remote terminal. I want to be able to copy and paste between remote terminals on two different computers. And I want to be able to take a screenshot on my phone and have it end up in the paste buffer on a remote Linux box. I took a run at solving this problem maybe three months ago, and I came at it from the wrong direction. I started to look at what it would take to integrate with Apple's iCloud-based copy-paste buffer magic syncing stuff. And I stepped back right at the point where it was going to involve reverse engineering iCloud crypto. Not because I didn't think my coding agents could do it, but because doing it felt like a great way to become a cautionary tale. But I kept being frustrated. So I took another swing at this sometime last month or so. And the result is called Clipfan. It runs as a menu bar app on your desktop Mac or Macs, and it integrates into tmux on all of your hosts. It uses SSH keys to set up a fully connected pasteboard syncing mesh. On the Mac, it has a pasteboard history because why not. It can auto-install across your fleet of computers and configure tmux on remote machines. It would, of course, be possible to configure it to sync pasteboards with other kinds of computers and other tools on those machines. ClipFan is free. It's available on GitHub today .

0 views
Justin Duke 1 weeks ago

Two hundred decisions

This week, after a little under two years of having adopted the practice, Buttondown has minted its 200th decision log. This is a practice very similar to RFCs or ADRs, but I prefer the term decision because it's both A, used for a variety of non-engineering purposes, and B, sounds a little less lame. The practice itself is extremely simple and there is no catch. It goes something like the following: Our decisions range from the monumental-yet-concise: to the trivial-but-nuanced: It is objectively not a free lunch in that it does take time for you to sit down and explain concisely in prose why you are doing a thing and what the other options are. It was most of all the null hypothesis of simply not acting. But a cost which serves as a forcing function to write, I mostly think of as a reward at this point. The world is complex: with every patch of fog that lifts, I find four more in the distance. 1 Only very recently have I started to understand knowledge as a perception of fog rather than a dispelling of it. And so I am very careful not to prescribe these days, unless I am so overwhelmingly confident in the universality of a given prescription. This is one such thing: keep a decision log. (See also Oxide's public RFD system .) Decisions are any non-trivial choice made for a specific reason. The context behind a decision is very hard to retrieve after the fact and grows in difficulty over time. The ability to revisit this context and update, invalidate, or buttress it is extremely useful in a variety of reasons.

0 views
matduggan.com 2 weeks ago

Clickhouse is winning the Observability Wars

For roughly the last ten years, a meaningful percentage of my working hours have been spent thinking about observability. If you're not familiar with the term, "observability" is what we call it now that "monitoring" doesn't sound expensive enough. The actual work is unglamorous in that you collect a lot of logs, some metrics, a few traces, and then you give them to people. I generally like my job. I like that we're always trying new ideas and approaches. I like the fact that when things go wrong, the answer is almost always sitting there in the data, waiting to be found by whoever is patient enough to look. But I want to be honest with you: in ten years of doing this work, across a half-dozen companies and every observability platform you've heard of and a few you probably haven't, logs have never stopped being the worst part of the job. They were the worst part when I started. They are the worst part today. I fully expect them to be the worst part of this job forever until the robots rise up and rip my head off in one clean sweep. I've written about why logs are terrible before , so I'll spare you the full lecture and give you the short version. Every developer's expectations for logs are set by a single formative experience: the syslog box. Or a container running locally. Or tail -f on a production server they probably shouldn't have SSH'd into. The point is that at some early, tender moment in their career, they had an experience with logs that was flawless. They ran and something useful came back. They piped it into jq and got exactly what they needed. This experience is the observability equivalent of a first kiss. It ruins them for everything that comes after. Because here is the thing about that flawless experience: it works because the system is small, the volume is trivial, and the person querying is the same person who wrote the log line. There is no schema drift, no cardinality explosion, no cross-team consumer with dashboard expectations, no VP asking why the "revenue events" graph has a gap in it. Then there are forty services. Now there are four hundred. Now the logs are being consumed not just by developers but by customer service, who need to look up a specific user's failed checkout from Tuesday. And by the data team, who are quietly building a business-critical dashboard on top of a log line that a backend engineer is about to refactor without telling anyone. And by the on-call, who at 3 AM does not want to learn a new query language, does not want to think about index patterns, and would like the search bar to just work. So you have a technical problem — the volume is enormous, the shape is inconsistent, the queries are unpredictable — sitting on top of an expectations problem, which is worse. Developers want logs instantly, they want to run arbitrary operations on them, and they will not commit to a schema. Meanwhile the less-technical consumers of that same data want the dashboards to be stable forever, the UI to be forgiving, and the whole thing to feel like a normal product. These two audiences are, in most practical respects, at war with each other, and you are the diplomat. ClickHouse came out of Yandex, where it was built to chew through analytical queries against absurd volumes of clickstream data. It was not designed for observability. It just happens to be shockingly good at it, because clickstream data and observability data have a lot in common: high volume, append-heavy, time-ordered, mostly read in aggregate, and every so often you need to reach in and find one specific needle. You can run it yourself with Helm charts. You can point Grafana at it via the ClickHouse plugin, or use their own web UI, or bring your own frontend. Their docs are actually good, which I mention because it's rare enough to be worth flagging. I've never used their ClickStack setup though, so YMMV. For observability specifically, the OpenTelemetry Collector has a ClickHouse exporter, which means you can pipe OTLP data straight in and let it manage the initial schema for you. ClickHouse is designed to scan billions of rows and ingest an amount of data that, when you first see the numbers, makes you assume they're lying. They're not lying. You query it with SQL, which is a language that already exists and was not created by a startup two weeks ago. I'm ranting about logs and then I'm explaining why I like to administer Clickhouse more. Let me take a second and explain why Clickhouse is really good at logs at scale. Logs, as a data shape, have some peculiar properties. They're append-only. You never update a log line, and you almost never delete a single one, though you delete a lot of them at once when retention kicks in. They arrive roughly in time order, though never actually in order. They're read in bursts where nobody looks at logs for days, and then during an incident somebody wants to scan a billion of them in seconds. They're highly compressible, because most of the bytes in your logs are repeated: the same service names, the same hostnames, the same error strings, the same JSON keys, over and over and over again. And critically, when you query them, you almost always want either a narrow time range across all fields or an aggregation across a wide time range with a few filters. You very rarely want "give me one specific row by ID" the way you would from a transactional database. (There are exceptions when its something like GDPR or compliance logging which is its own subgenre of nightmares). In a row-oriented database — Elasticsearch, Postgres, MySQL — the data for a single log line is stored together on disk. If your log has 40 fields and your query only cares about 3 of them, tough luck, you're reading all 40 from disk anyway. The database will filter it in memory, but the disk I/O has already happened. ClickHouse stores each column separately. If your query says SELECT service, status_code, count() FROM logs WHERE timestamp > now() - INTERVAL 1 HOUR GROUP BY service, status_code, ClickHouse reads exactly three columns off disk: timestamp, service, and status_code. The other 37 columns in your schema might as well not exist. On observability data, where you often have dozens of attributes but any given query touches three or four, this is the difference between scanning 800GB and scanning 40GB. This is also why the compression numbers look absurd. Columnar data compresses far better than row-oriented data because the values within a single column are, by nature, similar to each other. A column of service_name values might have a hundred distinct strings across a billion rows. ZSTD eats that for breakfast. You'll routinely see 10–14x compression ratios on real observability data, compared to 2–3x for Elasticsearch. The amazing thing is that ClickHouse scales without changing shape. I don't know how else to say this. Every other observability backend I've worked with mutates as it grows. The architecture at 1 TB a day and the architecture at 10 TB a day are recognizably different systems, with different failure modes, different ops burdens, and different mental models. ClickHouse at 10 TB a day looks like ClickHouse at 1 TB a day with more shards. That's it. That's the pitch. That's the whole reason I'm writing this. Let me show you what I mean. At 1 TB a day, every modern observability stack is roughly okay. If you're at this scale, you can pick almost anything and be productive. The differences below are real but they're not yet painful. Here is the honest truth: at 1 TB a day, ClickHouse is not less complicated than its peers. It's roughly the same. Maybe slightly more, if you count the schema design work you have to do up front. You get 10–14x compression with ZSTD and proper codecs, the Altinity Operator handles keeper coordination and the whole thing runs in about seven pods. But you do have to design your schemas. ORDER BY keys matter enormously. There is no native PromQL, so metrics workflows go through the Grafana plugin or through chproxy and an adapter. Roughly $1.5–2.5K/month. If you took the diagrams at this tier and squinted, you'd say they're all in the same weight class. And you'd be right. Now watch what happens next. This is where the exponential curve kicks in for everybody except one of these. You'll notice, if you look at the diagram, that I basically just added shards. That's it. That's the change. Same operator, same query engine, same query language, same mental model. Rebalancing after adding shards is manual, which is a real trade-off — most teams pre-provision or use weighting on Distributed tables to sidestep it. Materialized views for dashboard rollups shift from "nice to have" to "essential." Roughly $7–11K/month. The gap between ClickHouse and everything else opens up here. It doesn't close. This is where most solutions genuinely stop working, in the sense that even a well-staffed internal team cannot keep up with the operational load. If you've read this far, the point is probably already obvious, but I want to say it directly. Every observability stack works at 1 TB a day. If you're small, pick whatever your team already knows. Life is short. We're all just waiting for the robots to kick our heads off like soccer balls. The question is not which stack works today. The question is which stack still resembles itself two years from now, when your data volume has 5x'd and your team has 2x'd and the person who originally designed the whole thing has left the company. Elasticsearch mutates. LGTM mutates. Datadog stays operationally simple but mutates financially into something that requires its own dedicated team of accountants and pipeline engineers just to keep the bill from spiraling. ClickHouse just gets wider. You add shards. That's the whole trick. There is a real cost to this: you have to eat the schema-design and query-engine complexity up front, at a scale where the other options are objectively easier. You will be, briefly, the one making things harder for your developers. They will not always appreciate this. But the trade you're making is that their experience — and yours — remains roughly the same as the data grows by an order of magnitude, and the next order of magnitude, and probably the one after that. I have spent ten years watching observability stacks change shape underneath me while I tried to keep them running. ClickHouse is the first one that hasn't and that has been able to actually scale with me . That's pretty incredible. A relatively vanilla Elasticsearch cluster with Logstash providing some buffer between ingest and the Lucene indexes. Users get full-text search, which is genuinely good — this is the thing Elasticsearch is actually best at, and at this scale it delivers. Mapping explosions are already a background risk with mixed data, so dynamic mapping needs to be disabled or carefully templated from day one. ILM policies (hot → warm → delete) are non-optional even at this size, because forgetting to set them is how you get paged on a Saturday about disk pressure. Roughly $6–9K/month. Nothing too crazy. Alloy (formerly Grafana Agent, RIP) unifies the collection story into a single daemon, which is nice. Loki works well as long as you spend some time educating developers on how to attach useful labels — a conversation you will have many times, with many people, for the rest of your career. Mimir and Tempo largely do what it says on the tin. Roughly $3.5–5K/month. At 1 TB a day, Datadog is genuinely great. This is the scale it was built for, and it shows. You install the agent, you look at dashboards, you go home. There is almost nothing to think about, which is the entire point. You can already see the shape of the cost problem lurking in the diagram — the metered pipelines, the indexed-vs-ingested logs distinction, the custom metrics cardinality tax — but at this scale it's manageable. Roughly $45–75K/month, though negotiated pricing varies enough that I'd take that number with a grain of salt the size of a fist. Datadog's whole pricing philosophy is that they save you a full-time engineer. I think that framing is somewhat deranged, but they are extremely rich and I am not, so consider your source. Kafka is no longer optional. At 5 TB a day, direct writes into Elasticsearch cause bulk-reject storms and backpressure that will absolutely take your cluster down during a traffic spike. So now you're running Kafka, which means you're either running Kafka well or you're about to have a second, entirely different set of problems. Shard math becomes critical — at 50GB target shards, you're minting ~200 shards a day counting replicas, and your cluster state size becomes its own concern. You almost certainly need Elastic's commercial license for searchable snapshots and the frozen tier. Roughly $40–55K/month before licensing. That but Kafka You are now in microservices mode, whether you wanted to be or not. That means 65+ pods across three separate systems, each with its own compaction pipeline, its own hash ring, its own memcached tier. The gossip/memberlist ring becomes a real operational concern; ingester rollouts require careful -ingester.autoforget-unhealthy tuning, and if you get it wrong you either lose data or duplicate it. Roughly $22–32K/month. The operational complexity is still low, in that you don't run any servers. But you now need a full pipeline team whose entire job is reducing your Datadog bill. Exclusion filters, sampling rules, cardinality caps, tag allow-lists, the whole apparatus. This is what I call the "you build a system to avoid using the system you're paying for" trap, and once you're in it, you are in it forever. Roughly $180–350K/month, depending on how aggressive the pipeline team gets. This is also where you are basically fighting with your SaaS provider all the time, pouring over their billing documentation to figure out how to reduce costs. It's a hostile relationship and one I don't enjoy. You are now running three separate Elasticsearch clusters — one for logs, one for metrics, one for APM — federated through Cross-Cluster Search. Hot-tier NVMe cost dominates the bill. This is the scale at which teams start seriously evaluating alternatives, and where a lot of the recent migrations to ClickHouse have originated. Roughly $95–140K/month plus commercial licensing. You need people who are legitimate experts on Elasticsearch. Now thankfully Elastic just laid a ton of those people off, so they're probably possible to get, but still. Running this thing at this size is very complicated . Around 180+ pods, zone-aware everything, split-and-merge compaction, per-tenant limits, shuffle sharding to prevent noisy neighbors. You almost certainly have a dedicated observability platform team of three to five engineers at this point. If you don't, get ready for a bad fucking time. Roughly $55–85K/month. Still very easy to run, in the strict sense that you don't run anything. But your bill is now measured in six or seven figures a month, and the org has almost certainly built a pre-processing pipeline team whose entire existence is dedicated to reducing that bill. Most companies at this scale have gone hybrid: Datadog for APM and high-value metrics, self-hosted (increasingly ClickHouse) for logs. The complexity paradox at this scale is that you now have Datadog's simplicity plus your pipeline complexity plus a second self-hosted stack. Pricing is all over the goddamn place. You might be over a $1 million a month here. Look at the diagram and then look back at the 1 TB diagram. It's the same diagram. There are more shards. That's the difference. Materialized views for rollups are now mandatory rather than optional. Schema design mistakes you made two years ago will start to hurt, so hopefully you didn't make many. Rebalancing after adding shards is still manual; most teams pre-provision or use clickhouse-copier or a dual-write migration when they need to grow the cluster. Kafka starts to become useful as a buffer for very bursty ingest, though it's not required. Roughly $18–28K/month.

0 views
Lalit Maganti 2 weeks ago

On "When impressive performance gains do not matter"

When impressive performance gains do not matter is a very nice article covering some ways in which going after performance alone is not sufficient without considering the wider picture. It resonated a lot with how I think about performance. If there are multiple bottlenecks in the pipeline—and with these systems, this is common—the overall throughput will not improve until every last bottleneck is removed. His focus is on distributed systems bottlenecks, but I’ve hit the same “do-nothing” speedups when optimizing client side programs. Usually this comes from spending a lot of time thinking something was the bottleneck when it wasn’t. CPU profiling is where this bites me most: it tells me “function X is taking 30% of the cycles” and I think “oooo, there’s a lot of gains to be made there”. I build a microbenchmark for X, optimize it and there’s only a marginal gain at the high level. While disappointing, I’ve become used to it over time and internalized that performance is highly non-linear and actually knowing where the problem lies is really hard.

0 views
Filippo Valsorda 3 weeks ago

Vulnerability Reports Are Not Special Anymore

A requirement for staying sane while working in public as an open source maintainer is realizing that every issue, PR, and piece of feedback is a present, not an obligation. You can accept it, ignore it, and use it partially or not at all. For years, as lead of the Go Security team at the time, 1 I’ve told new team members that it doesn’t apply to vulnerability reports. No, vulnerability reports are special. Security researchers are doing us a favor by reporting things confidentially instead of doing full disclosure, so we owe them something, which is not true of regular issues opened on the issue tracker. 2 Different projects have different policies, but the general expectations are responsiveness and attribution. We’re supposed to acknowledge reports quickly, investigate them, keep the reporter posted, and eventually credit them with the discovery. Why? Well, because the reporter is providing us a service, not asking us to provide one (such as a bug fix or a feature implementation). In exchange for responsiveness and attribution, they are offering precious insight and the confidentiality we need to ship a fix before attackers ship an exploit. 3 Ultimately, it all stems from our responsibility to our users. The security researchers are not special, the insight and confidentiality are, and we need them to keep our users safe. Ignoring a security report communicates you don’t care about users’ security, and it’s rightly a reason for shame. It’s 2026 and none of the premises are true anymore. LLMs are as good as almost any security researcher, and anyone 4 can run them. The maintainers can run them. The attackers can run them. The insight is not scarce and precious anymore. The bottleneck now is not finding potential issues but assessing which ones are real. Unless there’s already a trust relationship, external researchers can’t meaningfully contribute to that triage process, and picking through an LLM’s output or through a inbox has approximately the same signal-to-noise ratio. Confidentiality, embargoes, and coordination also don’t matter nearly as much as they used to. The attackers don’t need to read the full disclosure post to learn about the vulnerability: they can ask their own LLM and, in fact, they also probably have the same triage bottleneck as the defenders do. The years of vulnerability reports being special might be over, as weird and uncomfortable 5 as that feels. Triage, rapid remediation, and—as ever—prevention are the job now. And we should all figure out how to run LLM analysis in CI, I suppose. For more, subscribe or follow me on Bluesky at @filippo.abyssdomain.expert or on Mastodon at @[email protected] . A few weeks ago, like every year, I ran the CENTOPASSI , a GPS-tracked motorcycle competition involving careful planning, 100 coordinates, and 1700 km of secondary roads over three days and a half. It always takes me to incredible places, like this abandoned bauxite mine in Puglia. My work is made possible by Geomys , an organization of professional Go maintainers, which is funded by Ava Labs , Teleport , Datadog , Tailscale , and Sentry . Through our retainer contracts they ensure the sustainability and reliability of our open source maintenance work and get a direct line to my expertise and that of the other Geomys maintainers. (Learn more in the Geomys announcement .) Here are a few words from some of them! Teleport — For the past five years, attacks and compromises have been shifting from traditional malware and security breaches to identifying and compromising valid user accounts and credentials with social engineering, credential theft, or phishing. Teleport Identity is designed to eliminate weak access patterns through access monitoring, minimize attack surface with access requests, and purge unused permissions via mandatory access reviews. Ava Labs — We at Ava Labs , maintainer of AvalancheGo (the most widely used client for interacting with the Avalanche Network ), believe the sustainable maintenance and development of open source cryptographic protocols is critical to the broad adoption of blockchain technology. We are proud to support this necessary and impactful work through our ongoing sponsorship of Filippo and his team. A role I passed on to capable hands when I left Google, so despite still being involved in the maintenance of the Go project, none of this is the official position of the Go Security team.  ↩ This gets messy quickly at the intersection of vulnerability report handling and Code of Conduct enforcement. If a security vulnerability is reported by someone who is also violating the CoC, what do you do? Do you ignore it? Fix it silently? Realistically, there’s no squaring the circle. It comes down to a judgment call based on how egregious the behavior is, on whether it is private or affecting the community, and on the resources available to the team members servicing . It’s an interesting job.  ↩ There’s actually a lot of complex history to disclosure practices, and in a different era it was genuinely dangerous to report security issues: well-intentioned researchers were frequently met with legal threats or prosecution. It took the full disclosure movement to make the industry internalize how counterproductive and unreasonable that was. Part of the coordinated disclosure (or “responsible” disclosure, a morally loaded term I dislike) trade was a promise, implicit or otherwise, not to go after researchers. Thankfully, that angle is mostly irrelevant to the reality of open source in 2026: no researcher fears prosecution in reporting a security vulnerability, and no project should even imply prosecution is on the table as the alternative to its documented reporting policy.  ↩ Welp. Sort of. But give it 1-3 months and the open models will catch up.  ↩ Just a few days ago, at the Geomys retreat, I was arguing that curl’s month-long suspension of vulnerability reporting channels was going too far, because it feels viscerally wrong to drop a security report on the floor. And yet, as I write this, I have no argument for servicing vulnerability reports being the best way to spend time to protect users. Gotta change to keep up with what the job actually is.  ↩ A role I passed on to capable hands when I left Google, so despite still being involved in the maintenance of the Go project, none of this is the official position of the Go Security team.  ↩ This gets messy quickly at the intersection of vulnerability report handling and Code of Conduct enforcement. If a security vulnerability is reported by someone who is also violating the CoC, what do you do? Do you ignore it? Fix it silently? Realistically, there’s no squaring the circle. It comes down to a judgment call based on how egregious the behavior is, on whether it is private or affecting the community, and on the resources available to the team members servicing . It’s an interesting job.  ↩ There’s actually a lot of complex history to disclosure practices, and in a different era it was genuinely dangerous to report security issues: well-intentioned researchers were frequently met with legal threats or prosecution. It took the full disclosure movement to make the industry internalize how counterproductive and unreasonable that was. Part of the coordinated disclosure (or “responsible” disclosure, a morally loaded term I dislike) trade was a promise, implicit or otherwise, not to go after researchers. Thankfully, that angle is mostly irrelevant to the reality of open source in 2026: no researcher fears prosecution in reporting a security vulnerability, and no project should even imply prosecution is on the table as the alternative to its documented reporting policy.  ↩ Welp. Sort of. But give it 1-3 months and the open models will catch up.  ↩ Just a few days ago, at the Geomys retreat, I was arguing that curl’s month-long suspension of vulnerability reporting channels was going too far, because it feels viscerally wrong to drop a security report on the floor. And yet, as I write this, I have no argument for servicing vulnerability reports being the best way to spend time to protect users. Gotta change to keep up with what the job actually is.  ↩

0 views
alikhil 3 weeks ago

Goodbye, nix-darwin!

Two years ago, my work laptop was force-updated by the IT team and got broken so badly that I had to set it up from scratch. I was frustrated. As an engineer, I don’t like repeating the same actions multiple times, and this gave me the motivation to set up my laptop as code. Some of my friends and colleagues were using Nix. When I decided to take a look, it looked promising: NixOS as a fully declarative OS, the nixpkgs package repository, reproducible environments, and the idea that almost everything can be described as code. There was also support for macOS: nix-darwin and home-manager, including Homebrew integration. I decided to give it a try. I set up my work laptop with it. Later, when I bought a Mac mini for personal use, I was able to reuse most of the config there too. This was cool. I could keep my shell, packages, editor settings, and many small tools in one repository. It felt like I finally had a proper source of truth for my machines. But it also made simple things more complicated. Before Nix, when I needed to add something new, I could install almost any app by running: With nix-darwin, the same action became a source code change followed by a slow rebuild command. Every change was tracked, reproducible, and easy to reuse on another machine. That was the whole point. But sometimes I just wanted to install a tool, try it for five minutes, and move on. Waiting for a rebuild each time made the whole setup feel heavier than I wanted. Over time, the cost became more visible in three places: updates, breakages after updates, and tools that expected a normal mutable macOS home directory. Updating packages was probably the most annoying part. In my setup, I usually did not update one small app directly. I updated the whole Nix configuration: flake inputs, nixpkgs, nix-darwin, home-manager, and then applied everything with . A small update could turn into a 30, 40, or 50 minute process. Nix had to evaluate the configuration, fetch new package versions, download or build what changed, update Homebrew packages through the generated Brewfile, and activate the new generation. I would start with “I need a newer version of this app” and end up maintaining my whole laptop. A new nixpkgs revision could change package options, rename something, move a config path, or slightly change how a module worked. Home Manager and nix-darwin also had their own options and behavior that changed over time. So after a big update, I often had to read error messages, search through changelogs or GitHub issues, and patch my config before the system could switch to the new generation. The rollback story is nice, and it is one of the strongest parts of Nix, but I still had to spend time understanding why the new generation did not build or activate. Package updates started to feel like small migration projects. nixpkgs itself was also inconvenient for me. It is a huge community-maintained package repository, and I respect the amount of work behind it, but new releases do not always appear there quickly. Sometimes the package I needed was behind the latest version. Sometimes it was missing a feature that had already been released upstream. And sometimes installing the latest version was possible, but required overrides, flakes, or other Nix-specific work that I did not want to do for a simple desktop app. This problem became much more visible during the last half year, when many AI tools started appearing. A lot of these tools are distributed through installer scripts: curl this shell script, run it, let it modify your shell config, add something to , install a binary somewhere, and maybe patch your environment. On a normal macOS setup, this usually works. But my shell config was managed by Nix and Home Manager. Some files were symlinks to generated files from the Nix store, some paths were not supposed to be edited manually, and the installer scripts had no idea about that. They tried to modify files that were managed elsewhere, failed, or produced broken changes. So while everyone else could try a new tool in a minute, I often had to stop and translate its installer into Nix config first. The setup stopped feeling helpful and started feeling like extra work. The breaking point happened when I changed my job. I tried to set up my new corporate laptop with Nix, but the company’s internal tooling wasn’t ready for that at all. Some tools expected the default macOS layout. Some scripts assumed Homebrew paths. Some security and management software did not play nicely with my setup. I spent some time trying to make it work, but it felt like I was fighting the company environment instead of doing my actual job. So I gave up and set up the laptop manually, without Nix. I kept using Nix at home for a while. But after that, my work and personal configurations diverged a lot. The main benefit of my setup was supposed to be reuse between machines. Once that disappeared, I had much less motivation to maintain the Nix config while still suffering from all the drawbacks described above. Eventually I decided that enough was enough. I pointed Codex to my Nix repository and gave it a task to de-Nix my Mac mini. Codex wrote a few migration scripts. It helped me move Homebrew packages out of the Nix-managed setup and back into normal Homebrew. It also helped me extract shell configuration into regular dotfiles. My Homebrew is Nix-free now, and I can directly edit my file again. I’m still scared to remove the directory though. It stays there for now. I have no problem with that. nix-darwin is an interesting tool. I still understand why people like it. Having your machine described as code is a nice concept. But for me, it was not user-friendly enough for daily macOS usage. I don’t want to debug my laptop configuration every time I need a new app. I don’t want to wait for a rebuild just to install a small tool. And I don’t want my personal setup to fight with corporate tooling. At this point, I would rather reinstall my system from scratch manually than spend 30 minutes installing a new Nix package.

0 views
Farid Zakaria 3 weeks ago

Nix needs relocatable binaries

This is my problem statement and proposal for a TacoSprint 2026 project 🏄. Nix, or store-based systems , are a class of package managers that use a well-defined prefix to store all packages. This can be for Nix or for Guix. This is simple. It makes rewriting paths to binaries or libraries easy. Derivations only need to the strings with the full store-path; becomes for instance. What if you wanted a different path, one not prefixed at the root ? This could be desirable if you don’t have Nix installed already or are missing necessary permissions – “rootless Nix”. Well, Nix already lets you specify a different store-path today but there is a catch! Let’s take a look at a simple example. We can build two different ways. The first command builds and installs at and the second at using and mount namespaces. Notice both have the same hash . This is important. By keeping the hash the same, we can leverage the precomputed derivations from binary substituters like https://cache.nixos.org . Ok, so what’s missing? If you are using tools like Bazel or Buck2 they likely already employ their own sandboxing via namespacing for builds. Integrating Nix into these ecosystems becomes incredibly impractical because we run into nested user namespace and mount restrictions. We can ask to use an alternate store prefix, without chroot and mount namespaces but it has a big gap. The hash is now 😭 It’s even more disastrous. Changing this simple string cascade-invalidates the entire dependency graph. You are now waiting 4 hours for GCC to compile just so you can print “Hello World” from a different folder. 🫠 This means we cannot leverage the public cache. This gap is called out by the Nix documentation today. Does it have to be that way? What if we could install Nix binaries anywhere , without using namespacing or . Can we have our cake and eat it too? 🍰 Nix needs relocatable binaries . The problem is that the store-prefix is part of the derivation itself so it affects the hash calculation. We don’t have to specify the full store-prefix everywhere. What if we used relative paths ? 🤔 Let’s look at one place the full paths are written today in the binary via . When this program runs, the dynamic linker looks at to find its shared dependencies. The loader in Linux however natively supports the variable which translates to “the directory containing the executable.” [ ref ] We could instead write the to be . If we did that then changing the store would cause no hashes to change. No recompilation. 🥳 Okay, so are we done? Well, like most things the devil is in the details. 😈 Before the dynamic linker can read the to find the necessary libraries, the Linux kernel has to load the dynamic linker itself. This path is stored in a different ELF header called (Program Interpreter). Unfortunately, the Linux Kernel does not support in this field as of today . We run into the exact same kernel limitation with the shebang line in scripts as well. When we execute a script, the kernel parses the (shebang) and expects an absolute path. Support for is also lacking as as of today . We cannot use relative paths reliably here unless they are relative to the current working directory, which breaks the moment you run the script from anywhere else. To achieve true relocatable binaries, we need to bypass these kernel limitations. historically would never make sense for in the Linux kernel because “Why would you want your dynamic linker to be found relative to the file!?”. Nix has changed that assessment. There are a few ways we could attack this: I believe augmenting support in the Linux kernel is the right approach. The beauty of Nix is we can even patch the kernel today in any NixOS machine for this support. As a final cherry on top, we can include additional metadata on every derivation whether it’s relocatable . 🍒 We could patch the Linux kernel so that is supported in and the shebang. We wrap every binary with a small static binary that computes its own location and then invokes the dynamic linker. We need to replace file locations to also leverage language-specific features for relative paths. For instance, in Python we can leverage to access files relative to itself similar to .

0 views
Jack Vanlightly 3 weeks ago

Raise the ambition threshold

“Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away.” — Antoine de Saint-Exupéry AI gives us an unprecedented ability to add. The danger is that we begin to mistake accumulation for value. Every new system and feature adds obligations: it must be operated, secured, monitored, documented, integrated, upgraded and eventually replaced or retired. Hackers love a juicy target, even if it’s that half-forgotten service that people are unsure whether it’s safe to turn off or not. If we respond to “cheaper” software creation by producing far more software, we may accumulate obligations faster than we acquire the capacity to discharge them. Under the weight of the proliferation of software, the organization starts to sacrifice its ability to build what it will need next to react effectively to changing market conditions and opportunities. This is the dynamic described by catabolic collapse . Catabolic collapse is a theory of societal decline in which a civilization accumulates more infrastructure than it can afford to maintain. Eventually, an increasing share of its available energy and resources is consumed merely preserving what already exists. Maintenance crowds out renewal. The society begins consuming its own capital simply to continue functioning. Think of debt payments taking up ever larger amounts of the national budget, the transport budget overwhelmed by the costs of fixing too many crumbling roads and bridges. If we accept that every organization, even with AI, has a finite capacity to maintain software, then it follows that we should select carefully the software projects we commit to. I can finally work on that feature that didn’t get funded time after time. I’m going to use AI to build it in two days rather than the estimated two weeks. This is a case of lowering the value threshold and it’s a sloppy way to introduce one of the most transformational technologies in human history. You might get lucky this time, it might end up worthwhile, but then you equally might just be adding that extra bell or whistle, meanwhile your competitor is building a revolutionary new product that will blow you out of the water. AI should raise the ambition threshold for software rather than lower the value threshold. Unless you’re in a small, agile start-up, building a highly strategic product still requires a lot of cross-organizational work. Software engineers, researchers, product managers, market research and customer feedback, the list goes on. But forget all that, let’s reward our engineers (generally focused more on technology than business value) for using huge numbers of tokens to build stuff without careful evaluation of the actual ROI of the work. It’s cool that Johnny finally rewrote that backend system in Rust, or rewrote the build system, or finally implemented that feature few customers actually are willing to pay for. But what was added may have done more for increasing the maintenance costs (and reducing the ability to react to future needs) than actually creating value. Prototyping and demos are another slippery slope. Prototyping is an ideal case for AI with its ability to accelerate work. However, if the prototype represents a system that falls into the category of “previously too low-value to justify,” then the prototype is part of the same problem. It seems that in the initial euphoria at the turn of the year at seeing the new power at our fingertips, some conflated faster for cheaper, more for better. The lesson is that we should continue to apply sensible constraints to what we build. Just because we can build it doesn’t mean that we should.  The danger of using AI injudiciously is greater in large organizations, where the average worker is farther away from the customer and the business value. The more disconnected you are from the success and failure of the organization, the easier it is for tokenmaxxing to help you spend time and money on producing a lot of lower value work. Add the slopification of work and some organizations might actually see a net-negative impact. Indiscriminate token usage in the large enterprise is already showing signs of faltering as CTOs question the value of their AI usage mandates. Business is a perpetual contest for advantage. Companies that spend their new AI capabilities trimming costs and burning down backlogs may soon be leapfrogged by competitors using them to attempt what was previously too difficult, risky or ambitious. So if you find that you are finally clearing all those nice-to-haves in the product backlog, ask yourself if your team is being ambitious enough.

0 views
Marc Brooker 3 weeks ago

Meet Alice. Alice is impatient.

What do you mean? Meet Alice. Alice uses your web service. Alice, like most humans, measures her time in seconds and minutes. Alice says your service is slow. You tell Alice that the mean request to your service completes in 100ms, but Alice says that her mean wait time is 1s. You’re both right. Meet Alex. Alex uses your web service. Alex, like most humans, measures his time in seconds and minutes. Alex says that when you have outages, they last a long time and he gets really annoyed. You tell Alex that your MTTR is less than 1 minute. Alex says that he sees the mean outage lasting 1 hour. Again, you’re both right. What’s going on? What’s going on is that you’re measuring time in requests, or in outages, and Alex and Alice are measuring time in seconds and minutes. When you have a long request or a long outage, Alex and Alice count that as a long time, with a heavy weight. But you only count that as one. More technically, what’s going on here is the inspection paradox . Alex and Alice don’t experience your latency distribution $f(t)$, they experience a t-weighted version of it. If you have a MTTR or mean request time of $\mathbb{E}[X]$, Alex and Alice experience $\mathbb{E}_a[X] = \frac{\mathbb{E}[X^2]}{\mathbb{E}[X]} = \mathbb{E}[X] + \frac{\mathrm{Var}(X)}{\mathbb{E}[X]}$. Most of the time they’re waiting, they’re waiting for things that take a long time. This is (roughly) how humans experience time. Let’s play with this with a little simulation. Plug in your median latency (or recovery time), and 99th percentile latency (and recovery time), we’ll fit a Gamma distribution to it, and then plot both what your service metrics see and what your customers see. Median: ms    p99: ms What your service sees (mean): – ms . What your customers experience (mean): – ms . For example, put in 30 as the mean (let’s ignore the milliseconds and pretend these are minutes for now) for a 30 minute Median TTR (i.e. in half of your postmortems you see a recovery time of $\leq 30$ minutes), and 600 in as the p99 (one in every 100 events, recovery takes 10 hours). Your MTTR is 81 minutes. Your customers experience a mean time to recovery of nearly 5 hours! There are many arguments for why tail latency (and long recovery times) are so important to understand (e.g. multiple samples ), but this is the one that I think is the least widely understood. For service times, timeout-and-retry can hide this latency some of the time (as long as the running request doesn’t hold locks or other exclusive resources). But, for recovery time, no such hiding is possible. The heaviness if the tail matters a great deal. This is also one of the reasons I don’t like trimmed measurements (like trimmed means) as a way of thinking about service latency or recovery time. They throw out some really critical context about the shape of the right tail that dominates the customer experience (the other reason is related to Little’s Law and capacity usage, which I’ve written about before ).

0 views
Nelson Figueroa 4 weeks ago

.gitignore Isn’t the Only Way To Ignore Files in Git

I’ve been using Git for so long and I just realized you can ignore files at three different levels and not just with . The three files you can use to ignore files are: is the usual file where you write files you want to ignore. It’s checked into Git along with the rest of the code. Whatever files you add to it will not get taken into account when running commands. The file lives in the directory of every Git repository but changes to it are not checked into Git. It usually has a few comment lines on a fresh Git repository. This file is useful for ignoring things on a per-repo basis. For example, you may have a personal file in a repository that you don’t want to check into git but you also don’t want to add to because it’s unique to your workflow. In that case you would add to . The file lives in your machine’s home directory in . Whatever filenames are added to this file are ignored globally at a machine-level. This file is not checked into Git and isn’t associated with any particular repository. It’s a great place to add files that you want to ignore in every git repository on your computer. For example, if you’re on macOS, adding here would be ideal. You can customize the global ignore file to be a different file. For example, if you want your global git ignore file to be you would run the command: And if you ever want to return to the default setting, run: When adding filenames to any of these, you can use this command to check how a filename is being ignored. For example, if you want to check how is being ignored, run in any Git repository. Here is the output when the repository’s is ignoring : Here is the output when the repository’s is ignoring : Here is the output when the global file is ignoring : And here is the output when a custom global ignore file is ignoring : If there is nothing ignoring a file, the command produces no output.

0 views
Blargh 4 weeks ago

I fixed shell pipes

In a previous post I made pipes in unix shells more reliable. Well, it had some drawbacks. I’ll summarize the problem, the failed previous version, and then show the new and improved one. Downstream processes in a unix shell pipe cannot know if the upstream finished successfully, or exited with an error. This means that it can’t know if it should “commit” the data it received. Example uses: In both of these cases you want the right hand side to STOP, and not finalize the upload or commit the transaction. This works fine for simple cases, but doesn’t support or per-command environment variables very well. And I don’t want to invent a complex language, so my replacement took a different path. wp on github . instead wraps the input and/or output with a very minimal encapsulating protocol. This allows normal data to pass through, but still allows the downstream to get as metadata. If the data stream ends before receiving the marker, then do not commit . The wrapped downstream child process sees this as remaining open, and instead it’s getting terminated with a signal. can either encapsulate when it wraps something that o utputs data, with , or decapsulate and receive the EOF marker when it’s handling i nput data, or both.

0 views
iDiallo 1 months ago

Debugging on Prod

The worst type of bug is one that only happens on prod. And only on prod. If you checked this blog in the past few weeks, you might have encountered a big fat 500 error. I'd had the same design for 10 years, and I wanted something fresh. But who can redesign without also improving the underlying code? I deleted a whole bunch of things: old templates that were never used, , a pile of unused CSS. I just had to. I deployed a first version and all the pages worked just fine. But then I got cocky. I decided to also improve the underlying code using GitHub Copilot. I was vigilant at first, reviewing every single line of generated code. None of it was complex really, just refactoring functions and the like. But along the way, I got lazy. I let the AI update deprecated functions on its own. The next time I deployed, the website returned a 500 error. When I checked the logs, nothing came back. No errors. I looked at running processes and noticed several PHP processes pinned at 100%. I reverted the code, but the server was still stuck. I restarted the web server, restarted PHP-FPM, and neither helped. The only thing that worked was restarting the whole machine. I ran the same code on my own machine and it worked fine. That's when I noticed I was running an older version of PHP on prod: PHP 8.3 vs. PHP 8.4 locally. No problem, I thought, and upgraded prod, which of course failed to fix anything. I waited for nighttime, redeployed the broken code, and debugged line by line until I found that Copilot had gone out of its way to "update" code in the Markdown library I use. If you know anything about Markdown, you know it's complex. This particular change was causing infinite recursion while parsing Markdown. I had no intention of reading through all that code to figure out exactly how it was failing, so I just reverted it. I redeployed and the problem seemed solved. Then I got an email: "Your website is down," a reader wrote in the middle of the night. While my American readers are asleep, Europeans are up bright and early reading my blog, for some reason (thank you, really). So debugging live on production was not an option. I reverted to the old code again. But how was the website still failing after I'd fixed the Markdown issue? And worse, it still worked fine locally. Just in case, I upgraded that very old Markdown library to something cooler and more modern: Parsedown . That didn't solve it either. The moment I deployed, the entire website failed, including pages that don't even use Markdown. Now it was personal. How do you debug a website that only fails in prod? I had a few tricks up my sleeve. First, I wrote a bash script to quickly switch between versions of the website. All it really did was flip a symlink between the "latest" folder and another folder I chose arbitrarily. Since I run PHP and every request is short-lived, I could switch to the broken version, debug, then switch back to the working version almost instantly. It's not like I have millions of readers hammering my server. This method worked, but it was slow, and it exposed internal information to the thousands of RSS readers scouring my website. Between 30,000 and 60,000 RSS reader requests hit the site daily. I couldn't afford to expose debugging code to that much traffic. So I used a second method: an even better way to debug live on prod without breaking URLs or throwing 500 errors at unsuspecting RSS readers. What if I ran both versions of the site simultaneously? Visit the regular domain and you'd get the latest working version. Visit a custom subdomain and you'd get the broken version. I achieved this by creating a new Apache configuration pointing to the latest (broken) path. This way, I had all the time in the world to debug the issue right on prod, without interfering with regular traffic. I eventually found the root cause. It was an orchestrated failure. Locally, I ran PHP directly. On prod, I ran PHP-FPM. Why the difference? Because Apache on prod runs HTTP/2 that requires an SSL connection, which I didn't need locally, and serving PHP over HTTP/2 requires PHP-FPM. PHP-FPM is essentially a process manager for your PHP instances. That explained the difference between the two setups, but not the actual cause of the bug. The real issue was in my caching mechanism. When a page is served from cache, I set the header: That's just a custom header. When the page isn't from cache, I set the value to . Here's the code that sets the headers: Now, what can go wrong here? When a page isn't served from cache, is set to . You see it now, don't you? evaluates to in PHP. So whenever a page wasn't served from cache, or the first time a page was hit after a deployment cleared the cache, this code ran instead: That's an invalid header. So why did it fail on prod but not locally? Because Apache silently ignores invalid headers, but PHP-FPM doesn't. It throws a 500 error: Headers need to follow the key-value rules defined in the internet standards (RFC 9110). Removing the condition and always using solved the problem. The blog engine runs on multiple machines I own locally. I never had to worry about the setup because both apache and php are tolerant to mistakes. In a talk, Rasmus Lerdorf once said that PHP works better when you don't know what you are doing. The header condition has its uses. For example, if you want to set that a page is 404 you can return: But I don't use this in my case. While copilot was of some help, it's a reminder that LLM generated code is to be treated with scrutiny. It reinforces my belief that I can never truly become a 10x engineer , because the more code I generate the more I have to review. And the more I trust it, the more likely it will bite my behind.

0 views
Lalit Maganti 1 months ago

TIL: Iroh: peer-to-peer networking for app developers

I came across Iroh ( via , via ) today as it hit 1.0 and found it a really interesting solution to a problem I knew existed but had not thought a lot about. Judging from the comment sections, it seems pretty clear that lots of people are confused as to exactly what Iroh is. I don’t think their launch post does their product justice at all, and their tagline is “IP addresses break, dial keys instead” which sounds cool, but if you think about it for just a second, you’ll end up with lots of questions. The biggest one is: “so how is this different from a mesh VPN like Tailscale, ZeroTier, Netbird, etc.?” It’s only after reading a lot of developers’ comments on the threads that I feel I understand: Iroh is aimed at  application  developers who want to communicate P2P between machines running their app, while mesh networks are aimed at  network admins  who want to connect devices they own/manage together.

0 views