{"id":3342,"date":"2026-07-02T03:39:49","date_gmt":"2026-07-02T03:39:49","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/07\/02\/i-spent-40-minutes-at-11pm-debugging-a-deploy-that-wasnt-broken\/"},"modified":"2026-07-02T03:39:49","modified_gmt":"2026-07-02T03:39:49","slug":"i-spent-40-minutes-at-11pm-debugging-a-deploy-that-wasnt-broken","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/07\/02\/i-spent-40-minutes-at-11pm-debugging-a-deploy-that-wasnt-broken\/","title":{"rendered":"I Spent 40 Minutes at 11pm Debugging a Deploy That Wasn&#8217;t Broken"},"content":{"rendered":"<div>\n<div><\/div>\n<p>The whole engine is <code>until \"$@\"; do ... done<\/code>. <code>until<\/code> runs the command and executes the loop body only when it <em>fails<\/em>, exiting the instant it succeeds. Passing the command as <code>\"$@\"<\/code> (after <code>shift<\/code>-ing past the attempt count) means the function retries <em>anything<\/em> \u2014 a <code>curl<\/code>, an <code>ssh<\/code>, a port check, your own script \u2014 without caring what it is.<\/p>\n<p>The backoff is the three lines at the bottom of the loop: sleep for the current delay plus a little jitter, then double the delay, capped at <code>max_delay<\/code>. That gives you 1s, 2s, 4s, 8s, 16s, 30s, 30s\u2026<\/p>\n<h2> <a name=\"the-jitter-is-not-decoration\" href=\"#the-jitter-is-not-decoration\"> <\/a> The jitter is not decoration <\/h2>\n<p><code>RANDOM % 3<\/code> looks trivial, and it&#8217;s the line people delete to &#8220;clean up.&#8221; Don&#8217;t. Without jitter, a fleet of machines that all failed at the same instant \u2014 because the same service went down \u2014 will all retry at the same instant, and the same instant after that, producing a synchronized thundering herd that knocks the recovering service straight back over on every round. A few hundred milliseconds of randomness per client spreads the retries out so the service actually gets room to recover. At one machine it does nothing; at fifty it&#8217;s the difference between recovery and a retry storm.<\/p>\n<h2> <a name=\"the-mistake-that-makes-retries-dangerous\" href=\"#the-mistake-that-makes-retries-dangerous\"> <\/a> The mistake that makes retries dangerous <\/h2>\n<div>\n<pre><code><span># Good: a transient failure that retrying can fix<\/span> retry 6 nc <span>-z<\/span> <span>-w<\/span> 2 db.internal 5432 <span># Bad: retrying a deterministic failure just delays the error 30 seconds<\/span> retry 6 curl <span>-fsS<\/span> https:\/\/api.example.com\/v1\/thing-that-returns-404 <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>A retry loop is only as smart as what you point it at. A port check belongs in a loop because the answer <em>changes<\/em> \u2014 &#8220;no&#8221; until the database boots, then &#8220;yes.&#8221; A request that returns 404 returns 404 on attempt one and attempt six; the loop just postpones the failure and buries the real status under retry noise. Retry transient failures \u2014 timeouts, connection-refused, 429, 5xx, DNS hiccups. Don&#8217;t retry deterministic ones \u2014 a 404, a 401, a syntax error, a missing file. When you can, branch on the exit code or HTTP status and loop only on the codes worth looping on.<\/p>\n<p>For plain <code>curl<\/code>, its built-in <code>--retry 5 --retry-delay 2<\/code> does most of this and is simpler; reach for the function when the thing you&#8217;re retrying isn&#8217;t curl, or when you want one backoff policy across a database probe, an ssh call, and a download at once.<\/p>\n<h2> <a name=\"back-to-11pm\" href=\"#back-to-11pm\"> <\/a> Back to 11pm <\/h2>\n<p>That deploy never paged me again once the migration <em>waited<\/em> for the port instead of assuming it. The database still took its six seconds to boot, the network still blipped occasionally \u2014 retrying didn&#8217;t make the dependencies faster. It stopped a normal, transient slowness from being treated as a fatal error, which is most of what &#8220;production-ready&#8221; means for a script.<\/p>\n<p>Full function with the wait-for-port pattern and the guidance on which failures to retry: <a href=\"https:\/\/bashsnippets.xyz\/snippets\/bash-retry-with-backoff\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/bashsnippets.xyz\/snippets\/bash-retry-with-backoff<\/a><\/p>\n<p>Retries are the third leg of an unattended job: <a href=\"https:\/\/bashsnippets.xyz\/snippets\/bash-flock-single-instance\" target=\"_blank\" rel=\"noopener noreferrer\">flock<\/a> stops overlap, <a href=\"https:\/\/bashsnippets.xyz\/snippets\/bash-timeout-command\" target=\"_blank\" rel=\"noopener noreferrer\">timeout<\/a> stops hangs, retry rides out the blip. The <a href=\"https:\/\/bashsnippets.xyz\/tools\/cron-wrapper-generator\" target=\"_blank\" rel=\"noopener noreferrer\">Hardened Cron Wrapper Generator<\/a> wires all three into one wrapper, <a href=\"https:\/\/bashsnippets.xyz\/guides\/bash-scripts-that-survive-cron\" target=\"_blank\" rel=\"noopener noreferrer\">Bash Scripts That Survive Cron<\/a> is the end-to-end version, and the rest of the library is at <a href=\"https:\/\/bashsnippets.xyz\/\" target=\"_blank\" rel=\"noopener noreferrer\">https:\/\/bashsnippets.xyz<\/a><\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/bashsnippets\/i-spent-40-minutes-at-11pm-debugging-a-deploy-that-wasnt-broken-1p2\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The whole engine is until &#8220;$@&#8221;; do &#8230; done. until runs the command and executes the loop body only when it fails, exiting the instant it succeeds. Passing the command as &#8220;$@&#8221; (after shift-ing past the attempt count) means the function retries anything \u2014 a curl, an ssh, a port check, your own script \u2014 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":3341,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[41],"tags":[],"class_list":["post-3342","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/3342","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=3342"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/3342\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/3341"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=3342"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=3342"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=3342"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}