<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yann Woo</title>
    <description>The latest articles on DEV Community by Yann Woo (@yannnnng).</description>
    <link>https://dev.to/yannnnng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195177%2Fb109c373-8e59-4df0-8a71-3d5183eda38b.png</url>
      <title>DEV Community: Yann Woo</title>
      <link>https://dev.to/yannnnng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yannnnng"/>
    <language>en</language>
    <item>
      <title>Four things that broke my job board (and the one I didn’t notice for two months)</title>
      <dc:creator>Yann Woo</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:41:43 +0000</pubDate>
      <link>https://dev.to/yannnnng/four-things-that-broke-my-job-board-and-the-one-i-didnt-notice-for-two-months-4o6k</link>
      <guid>https://dev.to/yannnnng/four-things-that-broke-my-job-board-and-the-one-i-didnt-notice-for-two-months-4o6k</guid>
      <description>&lt;p&gt;I’ve been building a remote job board &lt;a href="https://remotelanders.com" rel="noopener noreferrer"&gt;Remote Landers&lt;/a&gt;, since July. It runs on &lt;strong&gt;Next.js&lt;/strong&gt;, deployed to &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;, and the job data originally lived in &lt;strong&gt;Notion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This isn’t a tutorial. It’s a postmortem: in a little over three months, the site had four serious problems. I’ll go through them in the order they happened. Each one was harder to spot than the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. More jobs, and the site went down
&lt;/h2&gt;

&lt;p&gt;About 15 days after launch, things looked great. Google’s job search had started picking up our listings, and we were ranking well for a few keywords.&lt;/p&gt;

&lt;p&gt;Then we hit around 850 live jobs, and the site started crashing. Cloudflare was reporting that we’d exceeded resource limits.&lt;/p&gt;

&lt;p&gt;The cause wasn’t complicated. Some pages scanned every job in the database on every request, then picked out the few they needed. With a small table you’d never notice. With a bigger one, each request used more compute than Cloudflare’s free plan allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix had two parts:
&lt;/h2&gt;

&lt;p&gt;Replace “scan everything” with targeted queries. For example, a job detail page now looks up that one job by the identifier in its URL instead of walking the whole table.&lt;br&gt;
Upgrade to Cloudflare’s paid plan, which raises the per-request compute limit.&lt;/p&gt;

&lt;p&gt;This one was found fast, because the site simply wouldn’t load, and it was fixed fast too. But it left something behind: while the site was down, our listings disappeared from Google’s job search.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. We ran out of free GitHub Actions minutes
&lt;/h2&gt;

&lt;p&gt;Job data is pulled from companies’ hiring systems by scheduled GitHub Actions workflows. What I didn’t know at the start is that a free GitHub account gets 2,000 minutes of run time per month.&lt;/p&gt;

&lt;p&gt;I had several workflows running every few minutes. Before the month was over, the quota was gone, every workflow was stopped, and the job data stopped updating.&lt;/p&gt;

&lt;p&gt;To get the pipeline running again right away, I switched to a paid plan. Then I cleaned up: I removed the workflows that ran every few minutes but weren’t essential, and kept only the core ingestion jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson&lt;/strong&gt;: almost every free tool has a ceiling. It usually doesn’t warn you ahead of time. You find out when you hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. I spent a long time fixing a result
&lt;/h2&gt;

&lt;p&gt;The site was back up, but our listings never came back to Google’s job search.&lt;/p&gt;

&lt;p&gt;I assumed the problem was the job data itself, so I started digging into JobPosting structured data, the markup that tells Google “this page is a job.”&lt;/p&gt;

&lt;p&gt;I did find a real issue along the way. Many jobs were tagged as open worldwide, but when I sampled them, about one in four had a location restriction written right in the description. I changed the rule: a job is only tagged worldwide if the posting actually says so, and jobs with no location information aren’t submitted with that markup at all.&lt;/p&gt;

&lt;p&gt;I was also using Google’s Indexing API to notify Google about new and removed jobs. The default quota is 200 requests per day, and we add and remove far more than 200 jobs a day. One day’s log showed 491 of 701 requests rejected. I requested a higher quota and heard nothing back. I requested it again. Still nothing. I also discovered that more than half of our notifications were going to jobs that had already closed and were being re-sent over and over, so I fixed that too.&lt;/p&gt;

&lt;p&gt;All of that got fixed. The listings still didn’t come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A game showed me the real problem
&lt;/h2&gt;

&lt;p&gt;With JobPosting stuck, I was out of ideas. Around then I was playing a startup simulation game where you build and run a website.&lt;/p&gt;

&lt;p&gt;The game reduces running a website to a handful of key metrics. Response time is one of them, and a healthy site in the game sits around 30 ms. There’s no structured data in the game, no API quotas, just a few fundamentals.&lt;/p&gt;

&lt;p&gt;It made me realize that running a website should be simpler than I’d made it. If a game picks something as one of its few core metrics, it’s probably one that really matters. And JobPosting, the number I’d been staring at, was only a result.&lt;/p&gt;

&lt;p&gt;So I checked my own site’s response time: 1,500 ms. Fifty times the game’s number.&lt;/p&gt;

&lt;p&gt;Then I compared it with the crawl data in Google Search Console, and noticed the two moved in opposite directions: the slower the site, the less Google crawled it. Google’s own crawling documentation says the same thing: when a site responds quickly, Googlebot is willing to crawl more of it.&lt;/p&gt;

&lt;p&gt;That was the cause. The job data had always lived in Notion. I chose it because it’s easy to edit and you can see everything at a glance. The cost was speed: pages queried Notion at request time, and Notion’s API isn’t fast.&lt;/p&gt;

&lt;p&gt;This problem never crashed anything. Pages loaded fine, just slowly, so it went unnoticed for two full months. The CPU crash knocked our listings out of Google’s job search; slow responses kept them from coming back.&lt;/p&gt;

&lt;p&gt;I moved the job data out of Notion into a database on Cloudflare, so pages no longer ask Notion anything at request time. Response time dropped from 1,500 ms to around 400 ms.&lt;/p&gt;

&lt;p&gt;And then our listings started showing up in Google’s job search again. All the time I’d spent on structured data hadn’t brought them back. Fixing response time did.&lt;/p&gt;

&lt;p&gt;What I took away&lt;/p&gt;

&lt;p&gt;Looking back at all four, two things stand out.&lt;/p&gt;

&lt;p&gt;First, the more serious the problem, the quieter it tends to be. A crash gets noticed immediately. A slow site can go unnoticed for months.&lt;/p&gt;

&lt;p&gt;Second, it’s easy to keep fixing the result you can see. JobPosting was a number I checked every day, so I kept working on it. Response time was a number I wasn’t watching, so it never got fixed.&lt;/p&gt;

&lt;p&gt;These days I check response time regularly, to make sure it doesn’t quietly creep up again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re planning to use Notion or a similar tool as your backend, think about how you’ll keep response times down from the start.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>beginners</category>
      <category>notionchallenge</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
