<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Workharu — Blog</title>
  <subtitle>Notes on software, cloud and data engineering.</subtitle>
  <link href="https://www.workharu.com/feed.xml" rel="self" />
  <link href="https://www.workharu.com/" />
  <updated>2026-06-18T00:00:00Z</updated>
  <id>https://www.workharu.com/</id>
  <author>
    <name>Workharu</name>
  </author>
  <entry>
    <title>Running a GraphQL API on AWS Lambda, the good and the annoying</title>
    <link href="https://www.workharu.com/blog/serverless-graphql-lambda/" />
    <updated>2025-02-28T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/serverless-graphql-lambda/</id>
    <content type="html">&lt;p&gt;For a product with bursty traffic, or one that is just getting started, running a GraphQL API on AWS Lambda is hard to beat. You pay for what you use, it scales on its own, and there are no servers to keep patched. We have shipped exactly this setup, and it has worked well. But there are a few annoying bits worth knowing before you commit.&lt;/p&gt;&lt;h2 id=&quot;what-works-well&quot;&gt;What works well&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;The bill follows the usage.&lt;/strong&gt; An API that is quiet overnight costs almost nothing overnight.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Scaling you never think about.&lt;/strong&gt; Traffic doubles and Lambda just runs more copies. There is nothing to tune.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Small, safe deploys.&lt;/strong&gt; Each release is a version you can roll back in seconds.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;the-annoying-bits&quot;&gt;The annoying bits&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The first request is slow.&lt;/strong&gt; When Lambda spins up a fresh copy, that first request has to wait for it to start. For a Python app you have to keep the startup lean, and for the busiest paths you can pay a bit more to keep some copies warm.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Databases and Lambda do not love each other.&lt;/strong&gt; Hundreds of copies running at once can use up all of a database&#39;s connections. We lean on databases that cope with this, or put something in between that manages the connections for us.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;One heavy query can bite you.&lt;/strong&gt; GraphQL makes it easy to write a query that quietly turns into dozens of calls one after another. On Lambda that adds up and you feel it. Grouping those calls together and designing the queries well matters more here, not less.&lt;/p&gt;&lt;h2 id=&quot;how-we-keep-it-fast&quot;&gt;How we keep it fast&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;Keep the deployed code small, since slow startup means slow first requests.&lt;/li&gt;&lt;li&gt;Group and cache calls so one query is not fifty separate trips to the database.&lt;/li&gt;&lt;li&gt;Measure the slow requests, not just the average, because the slow starts hide in there.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Running GraphQL this way is not magic, but for the right kind of product it gives a small team an API that scales like a big one, with a bill that stays honest.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Thinking about serverless for your API? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;We are happy to sanity-check it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Five signs your nightly data job should run live instead</title>
    <link href="https://www.workharu.com/blog/building-real-time-data-pipelines/" />
    <updated>2025-03-12T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/building-real-time-data-pipelines/</id>
    <content type="html">&lt;p&gt;Running your data job once a night is a sensible starting point. It is simple, cheap, and easy to understand. But as a business grows, &amp;quot;the numbers update overnight&amp;quot; quietly goes from being fine to being a problem. Here are five signs it is time to switch to live data.&lt;/p&gt;&lt;h2 id=&quot;1-people-keep-refreshing-a-report-that-never-changes-during-the-day&quot;&gt;1. People keep refreshing a report that never changes during the day&lt;/h2&gt;&lt;p&gt;If someone checks a report at 2pm hoping it shows this morning&#39;s activity, and it never does, that is not them being difficult. Your data is just too old.&lt;/p&gt;&lt;h2 id=&quot;2-the-nightly-job-is-taking-longer-and-longer&quot;&gt;2. The nightly job is taking longer and longer&lt;/h2&gt;&lt;p&gt;When last night&#39;s job finishes at 5am instead of 2am, you are one busy week away from a job that does not finish before the work day starts.&lt;/p&gt;&lt;h2 id=&quot;3-you-keep-bolting-on-almost-live-hacks&quot;&gt;3. You keep bolting on &amp;quot;almost live&amp;quot; hacks&lt;/h2&gt;&lt;p&gt;Running the job every fifteen minutes, stacking one scheduled task on top of another. These are all signs the setup is fighting what you actually need.&lt;/p&gt;&lt;h2 id=&quot;4-people-are-making-decisions-on-old-numbers&quot;&gt;4. People are making decisions on old numbers&lt;/h2&gt;&lt;p&gt;The real cost is not technical, it is the business. Fraud caught hours too late, stock oversold, customers leaving before anyone notices.&lt;/p&gt;&lt;h2 id=&quot;5-fixing-yesterdays-data-is-a-nightmare&quot;&gt;5. Fixing yesterday&#39;s data is a nightmare&lt;/h2&gt;&lt;p&gt;If correcting yesterday&#39;s numbers means re-running a six-hour job and hoping nothing else breaks, your setup is too fragile for what you are asking of it.&lt;/p&gt;&lt;h2 id=&quot;how-to-move-without-starting-over&quot;&gt;How to move without starting over&lt;/h2&gt;&lt;p&gt;You rarely need to replace everything at once. What we usually do:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Add a live data feed next to the nightly job, running alongside it.&lt;/li&gt;&lt;li&gt;Move the most important data over to live first.&lt;/li&gt;&lt;li&gt;Keep the nightly job for old data where it still makes sense.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;You get the best of both: fresh data where it matters, and cheap and simple where it does not.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Not sure whether live data is right for you? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;Get in touch&lt;/a&gt; and we will give you a straight answer.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Long AI jobs need a proper tool, not a scheduled task and a prayer</title>
    <link href="https://www.workharu.com/blog/durable-workflows-temporal/" />
    <updated>2025-04-22T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/durable-workflows-temporal/</id>
    <content type="html">&lt;p&gt;The demo always works. You upload a file, the AI reads the text, works out its meaning, tags it, and files it away so you can search it. Nice and clean. Then you run it on ten thousand real files and reality shows up. A model times out. An outside service tells you to slow down. A worker restarts halfway through a job. Now what?&lt;/p&gt;&lt;h2 id=&quot;the-fragile-way&quot;&gt;The fragile way&lt;/h2&gt;&lt;p&gt;Most teams start by chaining things together. One message kicks off step one, which kicks off step two, and so on. It works until something fails halfway. Then you are stuck with hard questions:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Which step was this file on when the worker died?&lt;/li&gt;&lt;li&gt;Is it safe to try again, or will we charge someone twice or save the same thing twice?&lt;/li&gt;&lt;li&gt;How do we pick up the other 400 files that were partway through?&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You end up building your own tangle of retry logic and error handling in your database. You have reinvented a proper tool for this, badly.&lt;/p&gt;&lt;h2 id=&quot;the-reliable-way&quot;&gt;The reliable way&lt;/h2&gt;&lt;p&gt;A tool built for this (we use Temporal) changes how it works. You write the job as ordinary code, step one, step two, step three, and the tool makes sure it runs all the way through, exactly as you wrote it, even through crashes, restarts, and hours of waiting.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;It retries failed steps for you&lt;/strong&gt;, without you writing any retry code.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;It picks up where it left off.&lt;/strong&gt; If a worker dies, another one carries on from the same spot.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;You can see everything.&lt;/strong&gt; The full history of every job is there to look at, so you are not guessing where a file got stuck.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;when-it-is-worth-it&quot;&gt;When it is worth it&lt;/h2&gt;&lt;p&gt;You do not need this for a contact form. You do need it the moment a job:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Has several steps that can each fail on their own,&lt;/li&gt;&lt;li&gt;Runs long enough that a restart will interrupt some of them, or&lt;/li&gt;&lt;li&gt;Touches money, ownership, or anything you cannot safely do twice.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;For AI jobs like these, all three are true at once. That is why we build them this way from the start. It is far cheaper than bolting reliability on after the first thing goes wrong in production.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Building something with long or AI-heavy jobs? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;Let us talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Coming to the frontend from Java, what caught me off guard</title>
    <link href="https://www.workharu.com/blog/from-java-to-frontend/" />
    <updated>2025-11-05T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/from-java-to-frontend/</id>
    <content type="html">&lt;p&gt;I started out writing Java. Strong types, a compiler that argues with you before anything runs, a clear place for everything. When I moved into frontend work, a lot of it carried over more smoothly than people warned me it would. But a few things genuinely threw me, and I wish someone had just told me about them up front. So here they are.&lt;/p&gt;&lt;h2 id=&quot;the-compiler-stops-holding-your-hand&quot;&gt;The compiler stops holding your hand&lt;/h2&gt;&lt;p&gt;In Java, if I got a type wrong, I knew before I ran anything. The compiler would not let me get away with it. Plain JavaScript does not do that. It happily runs code that makes no sense and only falls over later, often somewhere far away from the actual mistake.&lt;/p&gt;&lt;p&gt;The thing that made me comfortable again was TypeScript. It brings back that &amp;quot;tell me now, not later&amp;quot; feeling I missed from Java. If you are coming from a typed language, do not spend long in plain JavaScript feeling like the floor is missing. Reach for TypeScript early. It thinks the way you already think.&lt;/p&gt;&lt;h2 id=&quot;nothing-waits-for-you&quot;&gt;Nothing waits for you&lt;/h2&gt;&lt;p&gt;This was the big one. In a lot of Java code, you ask for something and the line just waits until you get an answer before moving on. In the browser, if you waited like that, the whole page would freeze. So almost everything that takes time, fetching data, reading a file, talking to a server, happens without blocking, and you say what to do when the answer comes back later.&lt;/p&gt;&lt;p&gt;It took me a bit to stop fighting this and start thinking in terms of &amp;quot;this will finish eventually, here is what to do then.&amp;quot; Once it clicked, &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; made it read almost like normal step-by-step code again. But it is a real shift, and it is worth slowing down to actually understand it rather than copying patterns and hoping.&lt;/p&gt;&lt;h2 id=&quot;the-screen-is-a-reflection-of-your-data&quot;&gt;The screen is a reflection of your data&lt;/h2&gt;&lt;p&gt;The habit that made the biggest difference to my code was small: stop reaching into the page to change it by hand. In modern frontend work you keep your data in one place, and the screen is drawn from that data. You change the data, and the screen updates itself to match.&lt;/p&gt;&lt;p&gt;Coming from moving things around directly, this felt indirect at first. Now it feels obvious. When something on screen is wrong, I do not go hunting through the page. I look at the data behind it, because the screen is only ever showing me what the data says.&lt;/p&gt;&lt;h2 id=&quot;the-user-is-right-there-watching&quot;&gt;The user is right there watching&lt;/h2&gt;&lt;p&gt;The last thing is more a change of mindset than a technical one. A backend mistake is usually quiet. A frontend mistake is right in front of the person using the app: a button that does nothing, a form that loses what they typed, a spinner that never stops.&lt;/p&gt;&lt;p&gt;That sounds stressful, and at first it was. But it also made the work feel more direct. You are not building something abstract that runs in a data centre somewhere. You are building the thing a real person is looking at and touching. Getting the small stuff right, so it feels calm and predictable, turned out to be the part I enjoy most.&lt;/p&gt;&lt;p&gt;&lt;em&gt;None of my Java background went to waste, by the way. Caring about types, structure and edge cases is just as useful on the frontend. It just shows up in different places.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Our Docker images got smaller once we stopped overthinking them</title>
    <link href="https://www.workharu.com/blog/smaller-docker-images-faster-builds/" />
    <updated>2026-02-12T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/smaller-docker-images-faster-builds/</id>
    <content type="html">&lt;p&gt;For a while our Docker images were quietly awful. A gigabyte and a half for an app that was a few hundred lines of code. Builds that took long enough to go make tea. Nobody set out to make them that way. It just happened, one reasonable-looking line in the Dockerfile at a time.&lt;/p&gt;&lt;p&gt;Fixing it was not one clever trick. It was undoing a handful of small habits. If your images feel heavier than they should, it is probably these.&lt;/p&gt;&lt;h2 id=&quot;build-in-one-place-ship-from-another&quot;&gt;Build in one place, ship from another&lt;/h2&gt;&lt;p&gt;The biggest single win was splitting the build from the thing we actually run. You need a lot of stuff to &lt;em&gt;build&lt;/em&gt; an app: compilers, dev tools, the full set of packages. You need almost none of it to &lt;em&gt;run&lt;/em&gt; the finished thing.&lt;/p&gt;&lt;p&gt;A multi-stage build does exactly that. One stage installs everything and builds the app. The final stage starts fresh from a small base and copies over only the finished result. All the build tools, the caches, the extra packages, none of it comes along. That change alone took our images from &amp;quot;embarrassing&amp;quot; to &amp;quot;reasonable.&amp;quot;&lt;/p&gt;&lt;h2 id=&quot;start-from-a-smaller-base&quot;&gt;Start from a smaller base&lt;/h2&gt;&lt;p&gt;It is easy to reach for the full, general-purpose base image out of habit. It comes with a whole operating system&#39;s worth of things you will never use. The slim and alpine variants give you the same runtime with a fraction of the weight. Unless you have a specific reason to need the big one, start small. You can always add the one thing you are missing.&lt;/p&gt;&lt;h2 id=&quot;order-the-file-so-the-cache-actually-helps&quot;&gt;Order the file so the cache actually helps&lt;/h2&gt;&lt;p&gt;Docker builds in layers, and it reuses a layer if nothing above it changed. The mistake we kept making was copying all our code in early, before installing dependencies. That meant every tiny code change threw away the cached dependency install and did the whole slow thing again.&lt;/p&gt;&lt;p&gt;The fix is just order. Copy in the dependency list and install first. Copy the rest of the code after. Now changing a line of code reuses the cached dependencies, and the build that used to take minutes takes seconds. Same result, completely different feel to work with.&lt;/p&gt;&lt;h2 id=&quot;do-not-ship-things-you-do-not-need&quot;&gt;Do not ship things you do not need&lt;/h2&gt;&lt;p&gt;A &lt;code&gt;.dockerignore&lt;/code&gt; file is the quiet hero here. Without one, Docker hands your whole folder to the build, including the local dependency folder, the git history, environment files and whatever else is lying around. That makes the build slower and can even leak things you did not mean to include. List the junk in &lt;code&gt;.dockerignore&lt;/code&gt; and it stays out.&lt;/p&gt;&lt;h2 id=&quot;keep-it-boring&quot;&gt;Keep it boring&lt;/h2&gt;&lt;p&gt;That is genuinely the whole list. Split the build from the runtime, start from a small base, order the file so the cache works, and ignore the things you do not need. No exotic tooling, no clever hacks. Our images went from a headache to something we do not think about, and the builds got fast enough that nobody wanders off to make tea anymore.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Deploys feeling slow or heavier than they should? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;Reach out&lt;/a&gt; and we will help you tidy it up.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>Putting an LLM into production is mostly the boring parts</title>
    <link href="https://www.workharu.com/blog/llm-in-production-lessons/" />
    <updated>2026-04-10T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/llm-in-production-lessons/</id>
    <content type="html">&lt;p&gt;The first time I wired an LLM into a real product, the demo took an afternoon and I felt like a genius. Then it went in front of actual users and I spent the next three weeks on everything the demo had let me ignore. That gap, between &amp;quot;it works when I try it&amp;quot; and &amp;quot;it works when a thousand people try it,&amp;quot; is the whole job. Here is where the time actually goes.&lt;/p&gt;&lt;h2 id=&quot;the-demo-hides-all-the-hard-cases&quot;&gt;The demo hides all the hard cases&lt;/h2&gt;&lt;p&gt;In a demo you type a sensible question and get a sensible answer. Real users paste in half a sentence, a wall of text, another language, or something the model was never meant to handle. The model does not say &amp;quot;I am not sure.&amp;quot; It answers anyway, confidently, and sometimes it is wrong in a way that looks completely reasonable.&lt;/p&gt;&lt;p&gt;So the first real work is not the prompt. It is deciding what happens when the input is weird or the answer is low quality. What do you show the user? Do you fall back to something simpler? Do you just say you could not help this time? A good &amp;quot;I do not know&amp;quot; beats a confident wrong answer every single time, and you have to build that on purpose.&lt;/p&gt;&lt;h2 id=&quot;you-cannot-improve-what-you-cannot-measure&quot;&gt;You cannot improve what you cannot measure&lt;/h2&gt;&lt;p&gt;Early on I was tweaking prompts by feel. I would change a word, try three examples, decide it was better, and move on. That is not engineering, that is vibes.&lt;/p&gt;&lt;p&gt;What actually helped was boring: collect a set of real questions, write down what a good answer looks like for each, and run the whole set every time you change something. Now when I change a prompt I can see that it fixed four cases and broke two, instead of guessing. It is not fancy and it is not exciting, but it is the difference between improving the feature and just moving the bugs around.&lt;/p&gt;&lt;h2 id=&quot;cost-and-speed-are-features-not-afterthoughts&quot;&gt;Cost and speed are features, not afterthoughts&lt;/h2&gt;&lt;p&gt;Every call to a big model costs money and takes time. On a demo you never notice. In production, where the feature runs thousands of times a day, both add up fast.&lt;/p&gt;&lt;p&gt;A few things that consistently help:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Use the smallest model that does the job.&lt;/strong&gt; The biggest model is rarely worth it. Most tasks are fine on something cheaper and faster, and users feel the speed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Do not send the model work it does not need.&lt;/strong&gt; Trim the context. Half the cost is often text you attached out of habit.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Cache the answers you can.&lt;/strong&gt; If people ask the same thing, you do not need to pay for it twice.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;keeping-it-honest&quot;&gt;Keeping it honest&lt;/h2&gt;&lt;p&gt;The part I care about most is not letting the thing make things up. For anything where being wrong matters, we ground the model in real data we control and ask it to answer only from that, rather than from whatever it half-remembers. When it does not have the answer, it should say so.&lt;/p&gt;&lt;p&gt;None of this is glamorous. There is no clever prompt that makes it all go away. Shipping an AI feature that people actually trust is mostly measurement, guardrails and knowing what to do when the model is out of its depth. The demo is an afternoon. The rest is the job.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Thinking about adding an AI feature and want it to hold up in the real world? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;Talk to us&lt;/a&gt; and we will be honest about what is worth doing.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>The small frontend details that make an app feel finished</title>
    <link href="https://www.workharu.com/blog/small-frontend-details/" />
    <updated>2026-05-20T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/small-frontend-details/</id>
    <content type="html">&lt;p&gt;You can build an app that does everything it is supposed to and still have it feel rough. Everything works, but it feels a little cheap, a little unfinished, and users pick up on it even if they cannot say why. The difference is almost never one big thing. It is a handful of small details that nobody notices when they are done right, and everybody feels when they are missing.&lt;/p&gt;&lt;p&gt;These are the ones I check on everything I build.&lt;/p&gt;&lt;h2 id=&quot;tell-people-something-is-happening&quot;&gt;Tell people something is happening&lt;/h2&gt;&lt;p&gt;The moment a user clicks a button that talks to a server, there is a gap before anything comes back. If nothing changes on screen during that gap, it feels broken. So they click again. Now you have two requests and a confused user.&lt;/p&gt;&lt;p&gt;The fix is simple: the instant they click, show that you heard them. Disable the button, show a small spinner, change the label to &amp;quot;Saving.&amp;quot; It does not make anything faster, but it makes the wait feel handled instead of frozen. This one detail removes a surprising amount of frustration.&lt;/p&gt;&lt;h2 id=&quot;design-the-empty-state-on-purpose&quot;&gt;Design the empty state on purpose&lt;/h2&gt;&lt;p&gt;Every list starts empty. The first time a user opens the app, before they have added anything, they see the emptiest version of it. Too often that screen is just blank, which reads as broken.&lt;/p&gt;&lt;p&gt;An empty list is a chance, not a gap. A short line explaining what goes here and a button to add the first thing turns a dead end into a friendly starting point. It is one of the cheapest things you can do and one of the most noticeable.&lt;/p&gt;&lt;h2 id=&quot;say-what-went-wrong-in-plain-words&quot;&gt;Say what went wrong in plain words&lt;/h2&gt;&lt;p&gt;Things fail. The network drops, the server is busy, something times out. When that happens, &amp;quot;Error&amp;quot; or a raw technical message tells the user nothing except that they are stuck.&lt;/p&gt;&lt;p&gt;Tell them what happened and what to do about it, in normal words. &amp;quot;We could not save that, check your connection and try again&amp;quot; respects the person on the other end. And whenever you can, do not throw away what they typed. Making someone refill a form because your save failed is the fastest way to lose them.&lt;/p&gt;&lt;h2 id=&quot;let-the-keyboard-do-its-job&quot;&gt;Let the keyboard do its job&lt;/h2&gt;&lt;p&gt;Not everyone uses a mouse. Some people navigate with the keyboard by choice, some out of necessity. If you can tab through a form in a sensible order, submit it with Enter, and always see where you are, the app works for far more people. If you cannot, it quietly shuts some of them out.&lt;/p&gt;&lt;p&gt;You get most of this for free by using real buttons and real form fields instead of gluing click handlers onto plain elements. The browser already knows how these should behave. Let it.&lt;/p&gt;&lt;h2 id=&quot;respect-people-who-ask-for-less-motion&quot;&gt;Respect people who ask for less motion&lt;/h2&gt;&lt;p&gt;Animations can make an app feel nice. For some people they cause real discomfort. Browsers let a person say they prefer less motion, and it takes almost nothing to listen: keep the animation for everyone else, and for them, just skip to the result. It is a small kindness that costs you a couple of lines.&lt;/p&gt;&lt;h2 id=&quot;none-of-this-shows-up-on-a-feature-list&quot;&gt;None of this shows up on a feature list&lt;/h2&gt;&lt;p&gt;You will never see &amp;quot;the buttons show a spinner&amp;quot; or &amp;quot;the empty states are thought through&amp;quot; written on a spec. That is exactly why they get skipped, and exactly why doing them makes an app feel a class above one that did not bother. The features are what the app does. These details are how it feels to use, and that is the part people actually remember.&lt;/p&gt;</content>
  </entry>
  <entry>
    <title>How we keep our AWS bill from quietly creeping up</title>
    <link href="https://www.workharu.com/blog/keeping-aws-bill-under-control/" />
    <updated>2026-06-18T00:00:00Z</updated>
    <id>https://www.workharu.com/blog/keeping-aws-bill-under-control/</id>
    <content type="html">&lt;p&gt;An AWS bill almost never surprises you with one big jump. It creeps. A test environment nobody turned off, a log group keeping data forever, an instance that was sized for a launch that happened two years ago. Each one is small. Together they are a bill that makes everyone wince, and by then no one remembers what half of it is for.&lt;/p&gt;&lt;p&gt;I have cleaned up a lot of these, our own and clients&#39;. The fixes are almost never clever. They are just habits. Here are the ones that matter.&lt;/p&gt;&lt;h2 id=&quot;put-a-name-on-everything&quot;&gt;Put a name on everything&lt;/h2&gt;&lt;p&gt;The single most useful thing we do is tag every resource with what it is and which project it belongs to. It sounds like paperwork. It is the difference between &amp;quot;the bill is 4,000 dollars&amp;quot; and &amp;quot;this project is 4,000 dollars and 1,200 of that is one database nobody uses anymore.&amp;quot;&lt;/p&gt;&lt;p&gt;You cannot cut a cost you cannot see. Tagging is how you see it. Do it from day one, because doing it later, across hundreds of untagged resources, is miserable.&lt;/p&gt;&lt;h2 id=&quot;most-things-are-bigger-than-they-need-to-be&quot;&gt;Most things are bigger than they need to be&lt;/h2&gt;&lt;p&gt;When you set something up, you guess at the size, and people guess high to be safe. Then it runs at that size forever, using a fraction of what you are paying for.&lt;/p&gt;&lt;p&gt;Every few months we look at what things are actually using, not what we guessed. A server sitting at five percent load does not need to be that big. A database provisioned for a traffic spike that never comes can come down a size or two. This one review usually pays for itself many times over, and nothing breaks, because you are matching the size to the real work.&lt;/p&gt;&lt;h2 id=&quot;turn-off-what-nobody-is-using&quot;&gt;Turn off what nobody is using&lt;/h2&gt;&lt;p&gt;Development and test environments do not need to run at 3am on a Sunday. Setting them to shut down outside working hours is a small change that quietly removes a big chunk of waste, because those environments often cost as much as production while being used a fraction of the time.&lt;/p&gt;&lt;p&gt;The same goes for the leftovers: old snapshots, unattached disks, load balancers pointing at nothing. They each cost a little, every hour, forever. A cleanup every quarter keeps them from piling up.&lt;/p&gt;&lt;h2 id=&quot;do-not-keep-every-log-forever&quot;&gt;Do not keep every log forever&lt;/h2&gt;&lt;p&gt;Logs are cheap to make and easy to forget. By default a lot of them are kept forever, and &amp;quot;forever&amp;quot; gets expensive. Decide how long you actually need each kind, set that, and let the old ones age out on their own. You almost never need last year&#39;s debug logs, and if you do, you can keep the important ones somewhere cheaper.&lt;/p&gt;&lt;h2 id=&quot;set-an-alarm-before-it-matters&quot;&gt;Set an alarm before it matters&lt;/h2&gt;&lt;p&gt;The last piece is a budget alert. Pick a number that would worry you, and have AWS email you when the month is heading past it. It will not stop a runaway cost, but it means you find out on the second day, not when the invoice lands.&lt;/p&gt;&lt;p&gt;None of this is advanced. There is no secret discount. Keeping a cloud bill sensible is just paying attention on a schedule: tag things, right-size them, switch off what is idle, and let old data expire. Do that and the creep never gets a chance to start.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Bill quietly getting away from you? &lt;a href=&quot;https://www.workharu.com/contact/&quot;&gt;Get in touch&lt;/a&gt; and we will take a look and tell you where the easy wins are.&lt;/em&gt;&lt;/p&gt;</content>
  </entry>
</feed>