<?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: yuanke215</title>
    <description>The latest articles on DEV Community by yuanke215 (@yuanke215).</description>
    <link>https://dev.to/yuanke215</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%2F4145007%2Fd96ca73a-da4a-4ab1-8297-d98d3a66cbb6.png</url>
      <title>DEV Community: yuanke215</title>
      <link>https://dev.to/yuanke215</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuanke215"/>
    <language>en</language>
    <item>
      <title>401 vs 403 vs 404: The Status Code Mistakes That Break APIs</title>
      <dc:creator>yuanke215</dc:creator>
      <pubDate>Sun, 27 Sep 2026 03:11:16 +0000</pubDate>
      <link>https://dev.to/yuanke215/401-vs-403-vs-404-the-status-code-mistakes-that-break-apis-20a1</link>
      <guid>https://dev.to/yuanke215/401-vs-403-vs-404-the-status-code-mistakes-that-break-apis-20a1</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ipcalcplus.com/http-status-codes-guide.html" rel="noopener noreferrer"&gt;ipcalcplus.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every API review I've ever done contains at least one of these bugs: a login failure that returns 500, a rate limit that returns 400, a "resource not found" that returns 403 for security reasons and confuses every client developer for the next two years.&lt;/p&gt;

&lt;p&gt;Status codes are the API's contract with its clients. Break the contract and your consumers write defensive code around your bugs — code that then breaks when you fix the bug. This is a guide to the codes that actually matter, the distinctions people get wrong, and the two codes almost nobody uses correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-second mental model
&lt;/h2&gt;

&lt;p&gt;First digit = who broke it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2xx&lt;/strong&gt; — it worked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3xx&lt;/strong&gt; — go somewhere else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4xx&lt;/strong&gt; — the &lt;em&gt;client&lt;/em&gt; broke it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5xx&lt;/strong&gt; — the &lt;em&gt;server&lt;/em&gt; broke it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1xx&lt;/strong&gt; — hold on, I'm not done (mostly invisible; the one you'll see is 101 during a WebSocket upgrade)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole taxonomy. Everything interesting is in the distinctions within 4xx and 3xx.&lt;/p&gt;

&lt;h2&gt;
  
  
  401 vs 403: the distinction clients depend on
&lt;/h2&gt;

&lt;p&gt;The most misused pair in REST.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;401 Unauthorized&lt;/strong&gt; actually means &lt;em&gt;unauthenticated&lt;/em&gt;. The server does not know who you are. The client's correct response: acquire credentials (log in, refresh the token, add the API key) and retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;403 Forbidden&lt;/strong&gt; means &lt;em&gt;authenticated but not allowed&lt;/em&gt;. The server knows exactly who you are and is declining to serve you. The client's correct response: give up. Retrying with the same credentials will produce the same answer.&lt;/p&gt;

&lt;p&gt;When you return 403 for missing credentials, clients can't tell "I need to log in" from "I'm not permitted," and their retry logic breaks. The memorable version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;401 = "who are you?"&lt;/li&gt;
&lt;li&gt;403 = "I know who you are, and no."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One deliberate exception: many APIs return 404 for resources the caller isn't allowed to know exist — a private repo is "not found" rather than "forbidden," so the API doesn't leak which IDs are valid. That's a legitimate design choice; document it, because it's the opposite of what clients expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  404 vs 410, and the SEO angle nobody mentions
&lt;/h2&gt;

&lt;p&gt;404 Not Found is the most famous code and the most expensive one to misuse on the web. The nuance: &lt;strong&gt;410 Gone&lt;/strong&gt; means "this used to exist and was deliberately removed." Search engines treat 410 as a stronger signal than 404 — a 404 page may come back, a 410 page won't, so it gets deindexed faster.&lt;/p&gt;

&lt;p&gt;The related rule that preserves years of SEO work: when a URL moves permanently, &lt;strong&gt;301&lt;/strong&gt; (or &lt;strong&gt;308&lt;/strong&gt; for endpoints where the method must not change, like POST endpoints). A redesign that maps old URLs to 404 instead of 301 drops your search traffic within weeks — I've watched it happen, and the fix is always the same retrospective: export the old URL list, redirect everything, verify each target returns 200.&lt;/p&gt;

&lt;p&gt;Within redirects, the pair that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;301 vs 302&lt;/strong&gt;: permanent vs temporary. 301 transfers ranking to the new URL; 302 does not. Browsers convert POST to GET after a 301.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;307 vs 308&lt;/strong&gt;: same meanings, but method and body are preserved. For APIs, 307/308 are usually the correct choice; for public web pages, 301 remains the standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pair everyone underuses: 202 and 204
&lt;/h2&gt;

&lt;p&gt;Two codes that make APIs honest:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;202 Accepted&lt;/strong&gt; — "I received your request and will process it, but it isn't done." If your POST enqueues a job for async processing, 202 is the truthful answer. Returning 200 when the work hasn't happened yet means your client thinks the email was sent when it's actually still in a queue that will retry at 3 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;204 No Content&lt;/strong&gt; — "succeeded, and there's nothing to return." The clean response for DELETE, or any operation with no meaningful body. 204 also carries a subtle contract: the response &lt;em&gt;must&lt;/em&gt; have no body. Returning 200 with an empty string is what most APIs do, and it forces clients to handle two shapes for the same outcome.&lt;/p&gt;

&lt;p&gt;The general principle behind both: &lt;strong&gt;return the code that describes what actually happened, not the code that makes the client happy.&lt;/strong&gt; Fake-200 APIs push complexity downstream into consumer code, where it multiplies.&lt;/p&gt;

&lt;h2&gt;
  
  
  4xx codes that are worth their tokens
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;429 Too Many Requests&lt;/strong&gt; — you're rate limiting. Include a &lt;code&gt;Retry-After&lt;/code&gt; header; well-behaved clients honor it, and your effective load drops immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;422 Unprocessable Entity&lt;/strong&gt; — syntactically valid JSON, semantically wrong data (email field contains a phone number). Many APIs collapse this into 400, which forces clients to parse the error body to learn what kind of failure it was.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;415 Unsupported Media Type&lt;/strong&gt; — you sent XML to a JSON endpoint. Distinct from 400, and trivially cheap to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;451 Unavailable For Legal Reasons&lt;/strong&gt; — the takedown code, named after Fahrenheit 451. You'll rarely return it, but you should know why your favorite site vanished in some region.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the server is lying: 502, 503, 504
&lt;/h2&gt;

&lt;p&gt;The 5xx trio behind every "is it just me?" incident:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;502 Bad Gateway&lt;/strong&gt; — your proxy (nginx, the load balancer) reached the backend and got garbage back, or nothing. Usually means the backend crashed or is misconfigured. Read the &lt;em&gt;proxy's&lt;/em&gt; error log, not just the app log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;503 Service Unavailable&lt;/strong&gt; — deliberately refusing: overloaded, or in maintenance. This is the "back off" signal, and the right response from a circuit breaker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;504 Gateway Timeout&lt;/strong&gt; — the backend is up but too slow. Different fix from 502: this one is about timeouts and slow queries, not crashes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The debugging shortcut: 502/504 mean "the proxy couldn't talk to the app" and 503 means "the app or the operator chose to say no." Three different log files, three different fixes — which is why "the site is down" tickets get resolved so much faster when the first question asked is &lt;em&gt;which exact 5xx&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging workflow that works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reproduce with curl, capture headers, not just the body&lt;/li&gt;
&lt;li&gt;4xx? It's the request — validate payload, URL, auth header, Content-Type&lt;/li&gt;
&lt;li&gt;5xx? It's the server — application log first, proxy log second&lt;/li&gt;
&lt;li&gt;Read the response &lt;em&gt;headers&lt;/em&gt;: &lt;code&gt;Retry-After&lt;/code&gt;, &lt;code&gt;Location&lt;/code&gt;, &lt;code&gt;WWW-Authenticate&lt;/code&gt;, &lt;code&gt;Allow&lt;/code&gt; usually contain the actual fix&lt;/li&gt;
&lt;li&gt;Fix minimally, redeploy, retest with the exact same request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The habit that pays compound interest: save the full headers of every interesting failure. The status code names the problem class; the headers usually name the problem.&lt;/p&gt;




&lt;p&gt;If you want a searchable list of all this while you work — including the lesser-known codes and which fix goes with which — I keep a free &lt;a href="https://ipcalcplus.com/http-status-codes.html" rel="noopener noreferrer"&gt;HTTP status code reference&lt;/a&gt; and a longer &lt;a href="https://ipcalcplus.com/http-status-codes-guide.html" rel="noopener noreferrer"&gt;1xx-to-5xx guide&lt;/a&gt; at IPCalcPlus. Both run entirely in the browser, no signup. The &lt;a href="https://ipcalcplus.com/port-checker.html" rel="noopener noreferrer"&gt;Port Checker&lt;/a&gt; is next to it for the other half of "is it the network or the app" debugging.&lt;/p&gt;

&lt;p&gt;Which wrong status code do you see most often in the wild? My vote: 500 for expired tokens.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Five Cron Fields, One Trap: The Scheduling Bug Nobody Expects</title>
      <dc:creator>yuanke215</dc:creator>
      <pubDate>Sun, 27 Sep 2026 03:05:27 +0000</pubDate>
      <link>https://dev.to/yuanke215/five-cron-fields-one-trap-the-scheduling-bug-nobody-expects-17i2</link>
      <guid>https://dev.to/yuanke215/five-cron-fields-one-trap-the-scheduling-bug-nobody-expects-17i2</guid>
      <description>&lt;p&gt;Every backend eventually grows a cron job. Backups at 2 AM, digests at 8 AM, health checks every five minutes. And every team eventually hits the same wall: the schedule that fires at a time nobody asked for, in a timezone nobody configured, on a day-of-week nobody intended.&lt;/p&gt;

&lt;p&gt;Cron syntax is five fields. It fits on a sticky note. The bugs do not come from the syntax — they come from the three places where different implementations disagree with each other. This is a field guide to all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five fields, in one breath
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt; ┌───────────── &lt;span class="n"&gt;minute&lt;/span&gt; (&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="m"&gt;59&lt;/span&gt;)
 │ ┌───────────── &lt;span class="n"&gt;hour&lt;/span&gt; (&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="m"&gt;23&lt;/span&gt;)
 │ │ ┌───────────── &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; (&lt;span class="m"&gt;1&lt;/span&gt;-&lt;span class="m"&gt;31&lt;/span&gt;)
 │ │ │ ┌───────────── &lt;span class="n"&gt;month&lt;/span&gt; (&lt;span class="m"&gt;1&lt;/span&gt;-&lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;JAN&lt;/span&gt;-&lt;span class="n"&gt;DEC&lt;/span&gt;)
 │ │ │ │ ┌───────────── &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt; (&lt;span class="m"&gt;0&lt;/span&gt;-&lt;span class="m"&gt;6&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;SUN&lt;/span&gt;-&lt;span class="n"&gt;SAT&lt;/span&gt;; &lt;span class="m"&gt;7&lt;/span&gt; = &lt;span class="n"&gt;Sunday&lt;/span&gt; &lt;span class="n"&gt;too&lt;/span&gt;)
 * * * * *  &lt;span class="n"&gt;command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four operators cover 99% of real expressions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; — every value in the field&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;,&lt;/code&gt; — list: &lt;code&gt;6,18&lt;/code&gt; in hours = 6 AM and 6 PM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; — range: &lt;code&gt;MON-FRI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; — step: &lt;code&gt;*/15&lt;/code&gt; in minutes = every 15 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One subtlety worth internalizing: &lt;code&gt;*/15&lt;/code&gt; starts at the field's zero (0, 15, 30, 45), but &lt;code&gt;5/15&lt;/code&gt; starts at 5 (5, 20, 35, 50). The start value shifts the whole sequence — this is the difference between "aligned" and "offset" intervals, and it matters when you are spreading load across workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: day-of-month OR day-of-week
&lt;/h2&gt;

&lt;p&gt;Here is the bug I have now seen three times in production, always from a competent engineer, always written down as "cron is weird."&lt;/p&gt;

&lt;p&gt;The expression &lt;code&gt;0 0 1 * 1&lt;/code&gt; looks like it means "midnight on the first of the month, if it's a Monday."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In standard Unix cron, it means "midnight on the first of the month AND midnight on every Monday."&lt;/strong&gt; The relationship is OR, not AND. That job fires ~48 times a year instead of ~1.&lt;/p&gt;

&lt;p&gt;The rule, straight from the Vixie cron man page: when &lt;em&gt;both&lt;/em&gt; the day-of-month and day-of-week fields are restricted (neither is &lt;code&gt;*&lt;/code&gt;), the job runs when &lt;strong&gt;either&lt;/strong&gt; matches. To get true AND logic you need Quartz-style syntax (&lt;code&gt;0 0 1 ? * 1&lt;/code&gt;, where &lt;code&gt;?&lt;/code&gt; means "no specific value") or a guard in the script itself.&lt;/p&gt;

&lt;p&gt;This single rule explains most "my cron job runs at the wrong time" mysteries. If your target platform is Quartz, Spring, or Jenkins, check their docs — most treat the two day fields differently from Unix cron, which is exactly why the bug survives code review: it worked on the engineer's machine, and their machine was running a different scheduler.&lt;/p&gt;

&lt;h2&gt;
  
  
  The L, W, and # extensions (and where they get you fired)
&lt;/h2&gt;

&lt;p&gt;Modern schedulers — Quartz, Spring, AWS EventBridge, Kubernetes — support three extensions beyond classic cron:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;L&lt;/code&gt; — last: &lt;code&gt;0 0 L * *&lt;/code&gt; = last day of the month; &lt;code&gt;0 0 * * 5L&lt;/code&gt; = last Friday&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;W&lt;/code&gt; — nearest weekday: &lt;code&gt;0 0 15W * *&lt;/code&gt; = weekday closest to the 15th&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; — nth weekday: &lt;code&gt;0 0 * * 1#3&lt;/code&gt; = third Monday&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are genuinely useful for monthly report jobs. They are also &lt;strong&gt;silently invalid in classic Vixie cron&lt;/strong&gt; — the daemon treats them as syntax errors. Copy an &lt;code&gt;L&lt;/code&gt; expression from a Quartz tutorial into &lt;code&gt;/etc/cron.d/&lt;/code&gt; and your job simply never runs. No warning, no email, just silence.&lt;/p&gt;

&lt;p&gt;The portability rule: &lt;strong&gt;know your target before you copy the expression.&lt;/strong&gt; The quick reference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Fields&lt;/th&gt;
&lt;th&gt;L/W/#&lt;/th&gt;
&lt;th&gt;Timezone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unix crontab&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quartz / Spring&lt;/td&gt;
&lt;td&gt;6-7&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;JVM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes CronJob&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;node (or &lt;code&gt;timeZone&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS EventBridge&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UTC&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UTC&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Timezone and DST: the silent schedule-shifters
&lt;/h2&gt;

&lt;p&gt;Two production incidents waiting to happen:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. UTC defaults in the cloud.&lt;/strong&gt; "0 9 * * &lt;em&gt;" means 9 AM in *the scheduler's timezone&lt;/em&gt;. On GitHub Actions and AWS EventBridge, that is UTC. Your 9 AM daily report for the New York office goes out at 5 AM local. Nobody notices for a week because 5 AM email is just "early," until someone checks timestamps during a debugging session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Daylight saving transitions.&lt;/strong&gt; A job scheduled inside the spring-forward hour (e.g., 2:30 AM in a US timezone) &lt;strong&gt;does not run at all&lt;/strong&gt; that day — the wall-clock time never happens. During fall-back, a job in the repeated hour runs twice. If "runs exactly once per day" is a billing or audit requirement, that's a defect. The standard fix: schedule critical jobs in UTC, or outside DST transition windows.&lt;/p&gt;

&lt;p&gt;The general principle: for anything with an external SLA (backups, billing, cert renewal), &lt;strong&gt;write the cron expression in UTC and document that it is UTC.&lt;/strong&gt; For anything user-facing, use a scheduler that supports explicit timezones and set it deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six expressions worth memorizing
&lt;/h2&gt;

&lt;p&gt;You will type these for the rest of your career:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0 2 * * *&lt;/code&gt; — nightly at 2 AM (backups, the classic low-traffic window)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*/5 * * * *&lt;/code&gt; — every 5 minutes (health checks, watchdogs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0 9 * * MON-FRI&lt;/code&gt; — weekday mornings (reports, digests)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;15 4 * * 0&lt;/code&gt; — Sunday 4:15 AM (log rotation, cleanup)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0 */2 * * *&lt;/code&gt; — every 2 hours (syncs, cache refresh)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0 0 1 * *&lt;/code&gt; — first of the month (invoices, archives)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the debug checklist when a job "didn't run": (1) is the daemon alive? (2) is the expression valid &lt;em&gt;for this scheduler&lt;/em&gt;? (3) is the timezone what you think it is? (4) did a DST boundary swallow it? (5) check the scheduler's own log, not just your application log. In my experience the answer is (3) about half the time.&lt;/p&gt;




&lt;p&gt;When I'm porting a schedule between platforms I run it through a free &lt;a href="https://ipcalcplus.com/cron-expression-generator.html" rel="noopener noreferrer"&gt;Cron Expression Generator&lt;/a&gt; first — it parses the expression, describes it in plain English, and shows the next 10 run times, so a UTC bug or an OR-logic trap is visible &lt;em&gt;before&lt;/em&gt; it ships. It runs entirely in the browser. There's a longer reference writeup of everything above, including the full platform table, in the &lt;a href="https://ipcalcplus.com/cron-expressions-guide.html" rel="noopener noreferrer"&gt;Cron Expressions Guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the worst scheduling bug you've shipped? Bonus points if the root cause was a timezone.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>backend</category>
    </item>
    <item>
      <title>Stop Guessing Subnet Sizes: A Practical Mental Model for IPv4 Subnetting</title>
      <dc:creator>yuanke215</dc:creator>
      <pubDate>Sun, 27 Sep 2026 03:00:08 +0000</pubDate>
      <link>https://dev.to/yuanke215/stop-guessing-subnet-sizes-a-practical-mental-model-for-ipv4-subnetting-5f9n</link>
      <guid>https://dev.to/yuanke215/stop-guessing-subnet-sizes-a-practical-mental-model-for-ipv4-subnetting-5f9n</guid>
      <description>&lt;p&gt;Most people learn subnetting as a pile of formulas: count the bits, subtract two, memorize the table. That works for exams, but it falls apart the moment you are standing in front of a real network with an IP like &lt;code&gt;10.0.5.130/27&lt;/code&gt; and someone asks "what network is this on?"&lt;/p&gt;

&lt;p&gt;This article is the version I wish someone had given me: a single mental model that turns every subnetting question into arithmetic you can do in your head, plus the three shortcuts that make it fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea that makes subnetting click
&lt;/h2&gt;

&lt;p&gt;An IPv4 address is 32 bits. A prefix like &lt;code&gt;/26&lt;/code&gt; means: &lt;strong&gt;the first 26 bits are the network, the last 6 bits are the host.&lt;/strong&gt; That's it. Everything else follows from this sentence.&lt;/p&gt;

&lt;p&gt;The consequences of those 6 host bits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total addresses in the subnet: &lt;code&gt;2^6 = 64&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Usable hosts: &lt;code&gt;64 - 2 = 62&lt;/code&gt; (the first address is the network ID, the last is the broadcast)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice you never needed the subnet mask &lt;code&gt;255.255.255.192&lt;/code&gt;. The prefix and the mask are the same statement in two languages — but the prefix form is the one that keeps the math obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The block size shortcut (replaces binary math)
&lt;/h2&gt;

&lt;p&gt;Here is the shortcut that removes binary from your daily life:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block size = 256 minus the last non-255 octet of the mask.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/26&lt;/code&gt; → mask ends in &lt;code&gt;.192&lt;/code&gt; → block = &lt;code&gt;256 - 192 = 64&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/27&lt;/code&gt; → &lt;code&gt;.224&lt;/code&gt; → block = 32&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/28&lt;/code&gt; → &lt;code&gt;.240&lt;/code&gt; → block = 16&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have the block size, &lt;strong&gt;subnets can only start at multiples of that block&lt;/strong&gt;: 0, 64, 128, 192 for a /26. No exceptions. This is the single most useful fact in practical subnetting, because it means you can validate anyone's network design in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worked example: which subnet is 10.0.5.130/27 on?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Block size = 32&lt;/li&gt;
&lt;li&gt;List multiples: 0, 32, 64, 96, 128, 160...&lt;/li&gt;
&lt;li&gt;130 falls between 128 and 160 → network is &lt;code&gt;10.0.5.128/27&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Usable range: &lt;code&gt;.129&lt;/code&gt; to &lt;code&gt;.158&lt;/code&gt;, broadcast &lt;code&gt;.159&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You just did the entire subnetting calculation — network address, usable range, broadcast — with nothing but subtraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sizing: the question people actually get wrong
&lt;/h2&gt;

&lt;p&gt;The exam question "how many hosts in a /28?" is easy. The real question is "I have 28 devices — which prefix do I pick?" And here people consistently fail in the same direction: &lt;strong&gt;too tight.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;28 devices in a &lt;code&gt;/27&lt;/code&gt; (30 usable) = one printer installation from an outage&lt;/li&gt;
&lt;li&gt;The rule I use: &lt;strong&gt;current need + 30-50% headroom, then round up to the next prefix&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So 28 devices → aim for ~40 → &lt;code&gt;/27&lt;/code&gt; gives 30, not enough → &lt;code&gt;/26&lt;/code&gt; gives 62, take it. Wasted space is cheap. Re-numbering a live network is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three mistakes that cause real outages
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Misaligned boundaries.&lt;/strong&gt; Someone configures &lt;code&gt;192.168.1.50/26&lt;/code&gt; as a "new subnet." The mask zeroes the lower 6 bits, so the actual network is &lt;code&gt;192.168.1.0/26&lt;/code&gt; — the same subnet as the engineering VLAN. The config "looks valid" but silently overlaps. Rule: every network address must be a multiple of the block size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Mask mismatch after migration.&lt;/strong&gt; You re-subnet a segment from /24 to /26, but one server keeps the old /24 mask. It ARPs for addresses outside its real subnet, fails, and the ticket says "network is slow" — because the truth (half the hosts are unreachable) is invisible to the application team. Change masks in a maintenance window, all devices, all at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Forgetting the two reserved addresses.&lt;/strong&gt; &lt;code&gt;.0&lt;/code&gt; and &lt;code&gt;.255&lt;/code&gt; (or block edges on other prefixes) are not usable. A device handed a network or broadcast address fails in ways that depend on the OS — which means the symptom looks random.&lt;/p&gt;

&lt;h2&gt;
  
  
  When unequal subnets are the right answer
&lt;/h2&gt;

&lt;p&gt;Equal subnets are a training exercise. Real networks have one 60-person department and three 8-person teams, and giving everyone a /26 wastes three quarters of your address space. The professional answer is &lt;strong&gt;VLSM (Variable Length Subnet Masking)&lt;/strong&gt;: allocate the biggest subnet first, then fit the smaller ones into what remains.&lt;/p&gt;

&lt;p&gt;For a /24 with a 50-host engineering team, a 30-device guest network, and two small teams:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineering (50)&lt;/td&gt;
&lt;td&gt;/26&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.0/26&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guest (30)&lt;/td&gt;
&lt;td&gt;/27&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.64/27&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team A (12)&lt;/td&gt;
&lt;td&gt;/28&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.96/28&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team B (8)&lt;/td&gt;
&lt;td&gt;/28&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.112/28&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything fits in the bottom half, and the entire top half stays free for growth. Start big and work down — never the reverse, or you fragment the range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity-check any design in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Before you touch a router, verify three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Alignment&lt;/strong&gt; — every network address is a multiple of its block size&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No overlaps&lt;/strong&gt; — sort subnets by network address; each broadcast must be below the next network address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity&lt;/strong&gt; — usable hosts ≥ current devices × 1.3&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three pass, the design is sound. If any fails, you have just prevented an outage that would have been very hard to diagnose later — subnetting bugs don't show up as "subnetting bugs," they show up as "the network is weird."&lt;/p&gt;




&lt;p&gt;If you want to verify any of these calculations without doing them by hand — or you inherited a network and want to reverse-engineer which subnet an address belongs to — I maintain a free &lt;a href="https://ipcalcplus.com/subnet-calculator.html" rel="noopener noreferrer"&gt;Subnet Calculator&lt;/a&gt; that runs entirely in your browser (no signup, nothing leaves your device). There is also a &lt;a href="https://ipcalcplus.com/cidr-converter.html" rel="noopener noreferrer"&gt;CIDR converter&lt;/a&gt; for jumping between prefix and range notation, and a &lt;a href="https://ipcalcplus.com/subnet-mask-cheat-sheet.html" rel="noopener noreferrer"&gt;subnet mask cheat sheet&lt;/a&gt; worth pinning next to your terminal.&lt;/p&gt;

&lt;p&gt;What's your subnetting horror story? The misaligned-boundary one in the comments section is guaranteed to be good.&lt;/p&gt;

</description>
      <category>sysadmin</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
