<?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: Muhammad Ali Mughal</title>
    <description>The latest articles on DEV Community by Muhammad Ali Mughal (@muhammadalimughal).</description>
    <link>https://dev.to/muhammadalimughal</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%2F4145544%2F9c8ea5f5-70a1-4e76-875e-d25a0fd0cc55.png</url>
      <title>DEV Community: Muhammad Ali Mughal</title>
      <link>https://dev.to/muhammadalimughal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammadalimughal"/>
    <language>en</language>
    <item>
      <title>Laravel Interview Questions With Answers From Real Projects</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:12:36 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/laravel-interview-questions-with-answers-from-real-projects-5f7d</link>
      <guid>https://dev.to/muhammadalimughal/laravel-interview-questions-with-answers-from-real-projects-5f7d</guid>
      <description>&lt;p&gt;&lt;em&gt;By Muhammad Ali Mughal · Last updated 24 October 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most lists of Laravel interview questions test whether you've read the docs: "What is Artisan?", "What is a migration?". Interviewers who've shipped Laravel apps ask something else. They want to hear what broke, how you found it and what you changed. The best way to prepare is to answer each question with a mechanism and a real example, and to be ready to show it in code.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, a principal software engineer with 9+ years of PHP/Laravel and .NET work. At &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-interview-questions" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt; I've built Laravel apps for e-commerce, logistics and SaaS clients,. Below are 15 questions I'd ask a mid-level or senior Laravel developer, with short answers that show real experience, plus a small test I ran to back one of them up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmw51ukolo7mcnihzzvod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmw51ukolo7mcnihzzvod.png" alt="An open laptop with blurred code on a meeting room table, next to a glass of water and a notebook with a pen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel interview questions on Eloquent and the database
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What is the N+1 query problem, and how do you fix it?
&lt;/h3&gt;

&lt;p&gt;Looping over models and touching a relationship runs one query for the list plus one per row. Fix it with &lt;a href="https://laravel.com/docs/13.x/eloquent-relationships#eager-loading" rel="noopener noreferrer"&gt;eager loading&lt;/a&gt;: &lt;code&gt;Order::with('customer')-&amp;gt;get()&lt;/code&gt;. A strong answer adds how you &lt;strong&gt;catch&lt;/strong&gt; it: &lt;code&gt;Model::preventLazyLoading()&lt;/code&gt; in non-production environments, plus a query log or Debugbar during review.&lt;/p&gt;

&lt;p&gt;I checked this on PHP 8.4 with &lt;code&gt;illuminate/database&lt;/code&gt; v13.38.0 and an in-memory SQLite database seeded with 50 customers and 200 orders, counting queries with &lt;code&gt;DB::listen&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$queries&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;              &lt;span class="c1"&gt;// lazy&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// eager&lt;/span&gt;
&lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// aggregates&lt;/span&gt;
&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;chunkById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$chunk&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;               &lt;span class="c1"&gt;// batches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output (with the counter reset before each line):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lazy loading, 200 orders:      201 queries
Eager loading with('customer'): 2 queries
withCount + withSum, 50 customers: 1 queries (first: 4 orders, sum 6004)
preventLazyLoading: Illuminate\Database\LazyLoadingViolationException
chunkById(50) over 200 orders: 5 queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;201 queries down to 2. That's the kind of evidence I like to hear in an interview.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. When would you use &lt;code&gt;chunkById()&lt;/code&gt; instead of &lt;code&gt;get()&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;When processing thousands of rows in a job or command. &lt;code&gt;get()&lt;/code&gt; loads everything into memory. &lt;code&gt;chunkById()&lt;/code&gt; pages by primary key, so rows updated during the loop aren't skipped or repeated, which can happen with offset-based &lt;code&gt;chunk()&lt;/code&gt;. For read-only exports, &lt;code&gt;lazyById()&lt;/code&gt; gives the same safety with a simpler loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. How do you prevent two requests from overselling stock?
&lt;/h3&gt;

&lt;p&gt;Wrap it in &lt;code&gt;DB::transaction()&lt;/code&gt; and either lock the row with &lt;code&gt;lockForUpdate()&lt;/code&gt; or use an atomic conditional update: &lt;code&gt;Product::whereKey($id)-&amp;gt;where('stock', '&amp;gt;', 0)-&amp;gt;decrement('stock')&lt;/code&gt;, then check the affected row count. Mention that validation alone can't solve a race.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;$fillable&lt;/code&gt; or &lt;code&gt;$guarded&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Both protect against mass assignment. I prefer &lt;code&gt;$fillable&lt;/code&gt; on models that take user input, because new columns stay protected by default. The deeper answer: validate with Form Requests and pass &lt;code&gt;$request-&amp;gt;validated()&lt;/code&gt;, never &lt;code&gt;$request-&amp;gt;all()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. How do you write a migration that's safe on a large production table?
&lt;/h3&gt;

&lt;p&gt;Avoid long table locks. Add nullable columns first, backfill in batches through a job, then add constraints. Never edit a migration that has already run in production; write a new one. Senior candidates also mention testing &lt;code&gt;down()&lt;/code&gt;, or deciding consciously not to support it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel interview questions on queues and performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6. A queued job runs twice. Why, and what do you do?
&lt;/h3&gt;

&lt;p&gt;Usually &lt;code&gt;retry_after&lt;/code&gt; is shorter than the job's runtime, so the job is released while still running, or a worker crashed after the side effect but before acknowledging it. Fix the timing, then make the job idempotent: check state before acting, or use &lt;a href="https://laravel.com/docs/13.x/queues#unique-jobs" rel="noopener noreferrer"&gt;unique jobs&lt;/a&gt;. I covered the full setup in &lt;a href="https://dev.to/muhammadalimughal/running-laravel-queues-in-production-redis-supervisor-retries-and-failed-jobs-278m"&gt;Running Laravel Queues in Production&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Why doesn't my deployed fix take effect in queued jobs?
&lt;/h3&gt;

&lt;p&gt;Queue workers are long-running processes holding the old code in memory. Run &lt;code&gt;php artisan queue:restart&lt;/code&gt; (or &lt;code&gt;horizon:terminate&lt;/code&gt;) on every deploy. It's a small question, but it tells me whether someone has run Laravel in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. How do you rate-limit calls to a third-party API from jobs?
&lt;/h3&gt;

&lt;p&gt;Use &lt;a href="https://laravel.com/docs/13.x/queues#job-middleware" rel="noopener noreferrer"&gt;job middleware&lt;/a&gt; such as &lt;code&gt;RateLimited&lt;/code&gt; or &lt;code&gt;ThrottlesExceptions&lt;/code&gt;, together with &lt;code&gt;$tries&lt;/code&gt; and an increasing &lt;code&gt;$backoff&lt;/code&gt;. For SMS gateways and courier APIs I also log every request and response for support.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. What do you cache, and how do you invalidate it?
&lt;/h3&gt;

&lt;p&gt;Cache expensive, read-heavy data such as settings, menus and report totals with &lt;code&gt;Cache::remember()&lt;/code&gt;. Invalidate on write through model events or explicit &lt;code&gt;Cache::forget()&lt;/code&gt; calls. Good candidates mention cache tags on Redis, stampede protection with &lt;code&gt;Cache::lock()&lt;/code&gt;, and running &lt;code&gt;config:cache&lt;/code&gt; and &lt;code&gt;route:cache&lt;/code&gt; on deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel interview questions on architecture and security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  10. How does the service container help you?
&lt;/h3&gt;

&lt;p&gt;It resolves dependencies through constructor injection and lets you bind interfaces to implementations. A typical example: binding a &lt;code&gt;PaymentGateway&lt;/code&gt; interface to a JazzCash or Stripe class per environment, and swapping in a fake in tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Middleware, policies or gates for authorisation?
&lt;/h3&gt;

&lt;p&gt;Middleware handles request-level checks such as authentication. Policies hold model-level rules ("can this user update this order?"), gates hold ad-hoc abilities. For larger role systems I've used Spatie Permission with policies, so controllers only call &lt;code&gt;$this-&amp;gt;authorize()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. Where should business logic live?
&lt;/h3&gt;

&lt;p&gt;Not in controllers. I use action or service classes with a single public method, called from controllers, jobs and commands alike. That keeps logic testable and reusable. Strong candidates explain the trade-off instead of reciting a pattern name.&lt;/p&gt;

&lt;h3&gt;
  
  
  13. How do you protect a Laravel app against common attacks?
&lt;/h3&gt;

&lt;p&gt;Eloquent and the query builder use parameter binding against SQL injection, Blade's &lt;code&gt;{{ }}&lt;/code&gt; escapes output against XSS, and CSRF tokens are on by default for web forms. The mistakes come from &lt;code&gt;DB::raw()&lt;/code&gt; with user input, &lt;code&gt;{!! !!}&lt;/code&gt; on user content and missing authorisation checks on IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing and debugging questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  14. How do you test code that sends emails or calls APIs?
&lt;/h3&gt;

&lt;p&gt;Use fakes: &lt;code&gt;Mail::fake()&lt;/code&gt;, &lt;code&gt;Queue::fake()&lt;/code&gt;, &lt;code&gt;Event::fake()&lt;/code&gt; and &lt;code&gt;Http::fake()&lt;/code&gt; with &lt;code&gt;Http::preventStrayRequests()&lt;/code&gt;. Then assert what was sent. Feature tests with &lt;code&gt;RefreshDatabase&lt;/code&gt; cover the full request cycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  15. A page got slow in production. Walk me through it.
&lt;/h3&gt;

&lt;p&gt;Reproduce with real data, check query count and duration (Telescope, Debugbar or slow query log), look for N+1s and missing indexes with &lt;code&gt;EXPLAIN&lt;/code&gt;, then cache or queue what's left. The order matters more than the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prepare: a quick checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;What to have ready&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Eloquent&lt;/td&gt;
&lt;td&gt;One N+1 story with before and after query counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queues&lt;/td&gt;
&lt;td&gt;A duplicate-job or stuck-worker story and the fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data integrity&lt;/td&gt;
&lt;td&gt;How you handled a race condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Where you put business logic and why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Which fakes you use and one bug a test caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your CV&lt;/td&gt;
&lt;td&gt;Projects you can explain line by line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your applications aren't reaching the interview stage yet, my guide on &lt;a href="https://dev.to/muhammadalimughal/why-your-resume-gets-rejected-by-ats-and-how-to-fix-it-in-15-minutes-25aj"&gt;why resumes get rejected by ATS&lt;/a&gt; covers the step before this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Laravel interview questions are asked for experienced developers?
&lt;/h3&gt;

&lt;p&gt;Expect questions on N+1 queries and eager loading, queue reliability and idempotency, race conditions and transactions, caching and invalidation, authorisation with policies, and testing with fakes. Senior rounds add architecture trade-offs and production debugging stories, so prepare concrete examples with numbers rather than definitions alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should I prepare for a Laravel technical interview?
&lt;/h3&gt;

&lt;p&gt;Rebuild two or three features you've shipped as small demos you can explain line by line. Practise answering each question with the problem, the mechanism and what you'd change next time. Read the current Laravel docs for queues, Eloquent relationships and testing, because version details often come up.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the most common Laravel performance questions?
&lt;/h3&gt;

&lt;p&gt;The usual ones are N+1 queries, missing database indexes, loading too many rows with &lt;code&gt;get()&lt;/code&gt;, caching expensive queries, queueing slow work like emails and PDFs, and production commands such as &lt;code&gt;config:cache&lt;/code&gt;, &lt;code&gt;route:cache&lt;/code&gt; and OPcache. Be ready to explain how you measured the problem before fixing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Laravel interviews include live coding?
&lt;/h3&gt;

&lt;p&gt;Many do, usually a small task such as a CRUD endpoint with validation, a relationship query or fixing a failing test. Some companies send a take-home project instead. In both cases, clear structure, validation, tests and a short README tend to matter more than finishing every optional feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I learn Livewire or Inertia before a Laravel interview?
&lt;/h3&gt;

&lt;p&gt;Learn the one the job ad mentions. Livewire suits teams that want server-rendered interactivity in Blade, while Inertia suits teams using Vue or React. Knowing the core framework well matters more than either, but being able to discuss why a team might choose one is a plus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hiring a Laravel team instead?
&lt;/h2&gt;

&lt;p&gt;If you'd rather hire a team that has already answered these questions in production, see our &lt;a href="https://looporigin.com/services/laravel-development?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-interview-questions" rel="noopener noreferrer"&gt;Laravel development services&lt;/a&gt;, or visit &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-interview-questions" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which Laravel interview question caught you off guard? Share it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  laravel #php #interview #career #webdev #backend
&lt;/h1&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>laravel</category>
      <category>php</category>
    </item>
    <item>
      <title>How to Prevent Double Booking: Locks, Holds and Expiry</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:12:20 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-prevent-double-booking-locks-holds-and-expiry-5e54</link>
      <guid>https://dev.to/muhammadalimughal/how-to-prevent-double-booking-locks-holds-and-expiry-5e54</guid>
      <description>&lt;p&gt;&lt;em&gt;By Muhammad Ali Mughal · Last updated 24 October 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two customers click "Book" on the last slot at the same moment. Both see a confirmation. One of them shows up to find the truck, room or appointment already taken. If you want to prevent double booking, checking availability in your code before inserting isn't enough. The database has to enforce the rule, using a constraint, a row lock or both, and short-lived holds need an expiry so abandoned checkouts don't block real customers.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and at &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=prevent-double-booking" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt; we build booking and operations software, including a &lt;a href="https://looporigin.com/work/car-carrier-management-system?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=prevent-double-booking" rel="noopener noreferrer"&gt;Car Carrier Management System&lt;/a&gt; for a vehicle transport business in Pakistan, built on Laravel and PostgreSQL. In this guide I'll show you a race condition I reproduced on PostgreSQL 17, then three fixes and the test results for each.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63la3wrrijzz5wicjw2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63la3wrrijzz5wicjw2a.png" alt="Rack-mounted database servers with blue and green status lights in a small data center" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "check, then insert" double-books
&lt;/h2&gt;

&lt;p&gt;The usual first version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;carrier_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;during&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;'[2026-10-25 09:00+05, 2026-10-25 17:00+05)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- if 0, then:&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;during&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'c1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[...]'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under load, two requests can both run the &lt;code&gt;SELECT&lt;/code&gt;, both see zero, and both &lt;code&gt;INSERT&lt;/code&gt;. Nothing in the database says those rows conflict, so both commits succeed. This is a classic time-of-check to time-of-use race.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: 20 concurrent requests for one slot
&lt;/h2&gt;

&lt;p&gt;I wrote a Python script using &lt;code&gt;psycopg&lt;/code&gt; and 20 threads. Each thread opens its own connection, waits on a barrier so they all fire together, then tries to book carrier 1 for the same 8-hour window. Here's the naive version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM bookings WHERE carrier_id=1 AND during &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO bookings (carrier_id, customer, during) VALUES (1, %s, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result on my box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test 1: naive check-then-insert, one slot, 20 concurrent requests
  active bookings: 14 {'ok': 20}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fourteen bookings for a slot that should hold one, and every request reported success. The exact number changes from run to run, which is what makes this bug so hard to catch in manual testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Prevent double booking with an exclusion constraint
&lt;/h2&gt;

&lt;p&gt;For "one booking per resource per time range", PostgreSQL has a direct answer: an &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION" rel="noopener noreferrer"&gt;exclusion constraint&lt;/a&gt; over a &lt;a href="https://www.postgresql.org/docs/current/rangetypes.html" rel="noopener noreferrer"&gt;range type&lt;/a&gt;. It rejects any row whose range overlaps an existing row for the same carrier. The &lt;code&gt;btree_gist&lt;/code&gt; extension lets you mix plain equality on &lt;code&gt;carrier_id&lt;/code&gt; with the range overlap operator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;btree_gist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;no_overlap&lt;/span&gt;
  &lt;span class="n"&gt;EXCLUDE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;gist&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;carrier_id&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;during&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'held'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'confirmed'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;WHERE&lt;/code&gt; clause makes it a partial constraint, so cancelled or expired bookings don't block the slot. Same 20 threads, now just inserting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test 2: exclusion constraint on (carrier_id, during)
  active bookings: 1 {'ok': 1, 'ExclusionViolation': 19}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exactly one booking. The other 19 got error &lt;code&gt;23P01&lt;/code&gt; (exclusion violation), which your app can catch and turn into "Sorry, that slot was just taken".&lt;/p&gt;

&lt;p&gt;If your slots are fixed (for example 30-minute appointments), a plain &lt;code&gt;UNIQUE (resource_id, slot_start)&lt;/code&gt; constraint does the same job and works in MySQL too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Lock the parent row when capacity is more than one
&lt;/h2&gt;

&lt;p&gt;A car carrier has room for several vehicles, so the rule isn't "no overlaps", it's "no more than N". A constraint can't count rows, but a row lock can serialise the check. Lock the carrier row with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;, count, then insert, all in one transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capacity_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT capacity FROM carriers WHERE id=1 FOR UPDATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT count(*) FROM bookings WHERE carrier_id=1 AND during &amp;amp;&amp;amp; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AND status IN (&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CheckViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO bookings (carrier_id, customer, during, status, expires_at) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALUES (1, %s, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SLOT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, now() + interval &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10 minutes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With capacity set to 8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test 3: capacity 8 (car slots on one carrier), SELECT ... FOR UPDATE on the carrier row
  active bookings: 8 {'ok': 8, 'CheckViolation': 12}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight in, twelve refused. The lock makes each transaction wait for the previous one to commit before counting, as described in the PostgreSQL docs on &lt;a href="https://www.postgresql.org/docs/current/explicit-locking.html" rel="noopener noreferrer"&gt;explicit locking&lt;/a&gt;. Keep these transactions short, because everyone booking that carrier queues behind the lock.&lt;/p&gt;

&lt;p&gt;In Laravel, the same pattern is &lt;code&gt;DB::transaction()&lt;/code&gt; with &lt;code&gt;Carrier::whereKey($id)-&amp;gt;lockForUpdate()-&amp;gt;first()&lt;/code&gt; before the count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: Holds with expiry
&lt;/h2&gt;

&lt;p&gt;Notice that Test 3 inserted rows as &lt;code&gt;held&lt;/code&gt; with &lt;code&gt;expires_at = now() + 10 minutes&lt;/code&gt;. That's the pattern for checkout flows: reserve the slot while the customer pays, then confirm or release it.&lt;/p&gt;

&lt;p&gt;A hold that never expires is just another double-booking bug in slow motion. A scheduled job releases stale holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'held'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test it, I back-dated three holds, ran the sweeper, confirmed one hold as paid, and fired a second wave of 20 requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Test 4: expire stale holds, then capacity frees up
  holds expired: 3
  second wave of 20 requests: 8 {'ok': 3, 'CheckViolation': 17}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three slots freed, three new holds taken, total still capped at 8. Your availability query should also ignore expired holds directly (&lt;code&gt;expires_at &amp;gt; now()&lt;/code&gt;), so a late sweeper never shows a slot as full when it isn't.&lt;/p&gt;

&lt;p&gt;In Laravel I run the sweeper from the scheduler every minute, and payment callbacks go through queued jobs that confirm a hold only if it's still &lt;code&gt;held&lt;/code&gt;. I wrote about running those workers reliably in &lt;a href="https://dev.to/muhammadalimughal/running-laravel-queues-in-production-redis-supervisor-retries-and-failed-jobs-278m"&gt;Running Laravel Queues in Production&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which fix to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Works in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One booking per resource per time range&lt;/td&gt;
&lt;td&gt;Exclusion constraint on a range&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed slots (e.g. 30-minute appointments)&lt;/td&gt;
&lt;td&gt;Unique constraint on resource + slot start&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL, SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Capacity greater than one (seats, car slots, stock)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the parent row, or a conditional &lt;code&gt;UPDATE ... SET remaining = remaining - 1 WHERE remaining &amp;gt; 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout or payment step in between&lt;/td&gt;
&lt;td&gt;Holds with &lt;code&gt;expires_at&lt;/code&gt; plus a sweeper&lt;/td&gt;
&lt;td&gt;Any&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The conditional &lt;code&gt;UPDATE&lt;/code&gt; is worth knowing: it checks and decrements in one atomic statement, so there's no gap between check and write. The same idea protects stock counts in online stores, which I touched on in my guide to &lt;a href="https://dev.to/muhammadalimughal/launching-an-online-store-in-pakistan-a-practical-guide-to-platforms-payments-cod-and-hosting-155e"&gt;launching an online store in Pakistan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double booking prevention checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The database enforces the rule (constraint or lock), not only app code&lt;/li&gt;
&lt;li&gt;Time ranges use half-open intervals &lt;code&gt;[start, end)&lt;/code&gt; so back-to-back bookings don't clash&lt;/li&gt;
&lt;li&gt;Conflicts return a friendly "slot just taken" message, not a 500 error&lt;/li&gt;
&lt;li&gt;Holds have an expiry, and a scheduled job releases them&lt;/li&gt;
&lt;li&gt;Availability queries ignore expired holds&lt;/li&gt;
&lt;li&gt;Payment callbacks confirm a hold only if it's still active&lt;/li&gt;
&lt;li&gt;A concurrency test fires parallel requests at the same slot&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you prevent double booking in a database?
&lt;/h3&gt;

&lt;p&gt;Make the database enforce the rule. Use a unique constraint for fixed slots, an exclusion constraint for overlapping time ranges in PostgreSQL, or a row lock with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; when a resource has capacity above one. Checking availability in application code first, without a lock, still allows two requests to slip through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a unique index enough to stop double bookings?
&lt;/h3&gt;

&lt;p&gt;It's enough when bookings map to fixed slots, such as one row per resource per 30-minute start time. It isn't enough for free-form time ranges, because two different start times can still overlap. For ranges, use a PostgreSQL exclusion constraint, or lock the resource row before checking for overlaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between optimistic and pessimistic locking for bookings?
&lt;/h3&gt;

&lt;p&gt;Pessimistic locking takes a lock first, so other requests wait, as with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;. Optimistic locking lets everyone proceed and rejects the losers at write time, using a version column or a constraint violation. Optimistic suits low-contention bookings; pessimistic suits popular slots where many people compete for the same resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should a booking hold last?
&lt;/h3&gt;

&lt;p&gt;Long enough for a normal checkout, and no longer. Ten to fifteen minutes is a common choice for card payments, while a phone or bank transfer flow may need more. Store &lt;code&gt;expires_at&lt;/code&gt; on the hold, ignore expired holds in availability queries, and run a sweeper that marks them expired.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can MySQL prevent overlapping bookings?
&lt;/h3&gt;

&lt;p&gt;MySQL has no exclusion constraints, so you need another approach. Use fixed slots with a unique index, or lock the resource row with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; inside a transaction before checking for overlaps. Both work with InnoDB. Test either approach with parallel requests, not just one browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need a booking system built?
&lt;/h2&gt;

&lt;p&gt;If you're planning a booking, dispatch or reservation platform, see how we handled bookings, dispatch and accounting in the &lt;a href="https://looporigin.com/work/car-carrier-management-system?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=prevent-double-booking" rel="noopener noreferrer"&gt;Car Carrier Management System&lt;/a&gt;, or visit &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=prevent-double-booking" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have you been bitten by a double-booking bug in production? Tell me how you found it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  systemdesign #postgresql #database #backend #laravel #architecture
&lt;/h1&gt;

</description>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>backend</category>
      <category>scalability</category>
    </item>
    <item>
      <title>How Long Should a Password Be? Password Entropy Explained</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:12:04 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-long-should-a-password-be-password-entropy-explained-549o</link>
      <guid>https://dev.to/muhammadalimughal/how-long-should-a-password-be-password-entropy-explained-549o</guid>
      <description>&lt;p&gt;&lt;em&gt;By Muhammad Ali Mughal · Last updated 24 October 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"At least 8 characters, one uppercase, one number, one symbol." Most of us learned password rules from sign-up forms, and most of those rules are out of date. So how long should a password be? For anything that protects an account on its own, use at least 15 random characters, or a passphrase of 5 or more random words, and let a password manager remember it. Length and randomness matter far more than symbols.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I build &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=password-length" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;, a set of free privacy and developer tools. In this guide I'll show you the maths behind that answer, the actual numbers I got when I ran it on my machine, and what current guidance from NIST says.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6jmyzxocr6fel6i63d3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh6jmyzxocr6fel6i63d3.png" alt="A laptop on a desk with blurred code on screen, a phone, a desk lamp and a notebook with a key fob" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What password entropy means
&lt;/h2&gt;

&lt;p&gt;Entropy is a measure of how many guesses an attacker needs. For a password generated at random, it's simple to calculate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;entropy (bits) = length × log2(size of the character pool)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each extra bit doubles the number of possible passwords. A random 8-character password drawn from all 94 printable ASCII characters has about 52 bits. A random 16-character one has about 105 bits. That isn't twice as strong. It's 2^52 times as many possibilities.&lt;/p&gt;

&lt;p&gt;There's one catch: the formula only holds for &lt;strong&gt;random&lt;/strong&gt; passwords. &lt;code&gt;Summer2026!&lt;/code&gt; uses uppercase, lowercase, digits and a symbol, but attackers try season-plus-year patterns early. Its real strength is a tiny fraction of what the formula suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long should a password be? I ran the numbers
&lt;/h2&gt;

&lt;p&gt;I wrote a short Python script that generates a sample password with the &lt;code&gt;secrets&lt;/code&gt; module (Python's cryptographically secure random source) and estimates the average time to crack each format. I assumed an offline attack at 10 billion guesses per second, which is a reasonable figure for a fast, unsalted hash on GPU hardware. Real speeds vary a lot with the hash used, so treat this as a comparison, not a promise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;

&lt;span class="n"&gt;GUESSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1e10&lt;/span&gt;  &lt;span class="c1"&gt;# guesses per second, offline, fast hash
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;GUESSES&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bits=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  avg crack: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;years&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; years&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;full&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ascii_letters&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;punctuation&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sample:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Random, all 94 printable, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Passphrase, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; words (7776 list)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7776&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what it printed (I've rounded the times for readability):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Password type&lt;/th&gt;
&lt;th&gt;Entropy&lt;/th&gt;
&lt;th&gt;Average time to crack at 10^10 guesses/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8 random characters (94 pool)&lt;/td&gt;
&lt;td&gt;52.4 bits&lt;/td&gt;
&lt;td&gt;3.5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 random characters&lt;/td&gt;
&lt;td&gt;78.7 bits&lt;/td&gt;
&lt;td&gt;about 755,000 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 random characters&lt;/td&gt;
&lt;td&gt;98.3 bits&lt;/td&gt;
&lt;td&gt;about 6.3 × 10^11 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 random characters&lt;/td&gt;
&lt;td&gt;104.9 bits&lt;/td&gt;
&lt;td&gt;about 5.9 × 10^13 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15 random lowercase letters&lt;/td&gt;
&lt;td&gt;70.5 bits&lt;/td&gt;
&lt;td&gt;about 2,660 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-word passphrase (7,776-word list)&lt;/td&gt;
&lt;td&gt;51.7 bits&lt;/td&gt;
&lt;td&gt;2.1 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5-word passphrase&lt;/td&gt;
&lt;td&gt;64.6 bits&lt;/td&gt;
&lt;td&gt;about 45 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-word passphrase&lt;/td&gt;
&lt;td&gt;77.5 bits&lt;/td&gt;
&lt;td&gt;about 351,000 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-digit PIN&lt;/td&gt;
&lt;td&gt;19.9 bits&lt;/td&gt;
&lt;td&gt;0.05 milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things stand out. First, 15 random &lt;strong&gt;lowercase&lt;/strong&gt; letters beat 8 characters that use every symbol on the keyboard. Length wins. Second, a 4-word passphrase is about as weak as an 8-character password, which is why 5 or 6 words is the safer choice.&lt;/p&gt;

&lt;p&gt;The 7,776-word list size comes from the &lt;a href="https://www.eff.org/dice" rel="noopener noreferrer"&gt;EFF dice wordlists&lt;/a&gt;, built for picking words at random with dice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What NIST says about password length
&lt;/h2&gt;

&lt;p&gt;The US National Institute of Standards and Technology updated its digital identity guidelines in &lt;a href="https://pages.nist.gov/800-63-4/sp800-63b.html" rel="noopener noreferrer"&gt;SP 800-63B-4&lt;/a&gt;. The parts that matter for everyday users and developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passwords used as the &lt;strong&gt;only&lt;/strong&gt; factor must be at least &lt;strong&gt;15 characters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Passwords used as part of multi-factor login may be shorter, but at least &lt;strong&gt;8 characters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Services should allow a maximum length of &lt;strong&gt;at least 64 characters&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Services shall not force composition rules like "one symbol, one number"&lt;/li&gt;
&lt;li&gt;Services shall not force periodic password changes, only a change when there's evidence of compromise&lt;/li&gt;
&lt;li&gt;Chosen passwords should be checked against a blocklist of common and breached values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the "8 characters plus a symbol" rule you see on many sign-up forms falls well short of current guidance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password length by use case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;My recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accounts stored in a password manager&lt;/td&gt;
&lt;td&gt;16 to 20 random characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password manager master password&lt;/td&gt;
&lt;td&gt;6 random words, or more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi (WPA2/WPA3) password&lt;/td&gt;
&lt;td&gt;16+ random characters or 5+ random words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device or laptop login you type daily&lt;/td&gt;
&lt;td&gt;5+ random words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone or card PIN&lt;/td&gt;
&lt;td&gt;6+ digits, only where wrong guesses are rate limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API keys and app secrets&lt;/td&gt;
&lt;td&gt;Generated with 128+ bits of randomness, never typed by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on PINs: 6 digits is almost nothing offline, but a phone that locks after a few wrong guesses changes the maths. In the same script, a 6-digit PIN limited to 10 guesses per hour takes an average of 5.7 years. Short secrets are only acceptable when guessing is throttled.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to create a strong password in 3 steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't invent it.&lt;/strong&gt; Humans pick patterns. Use a generator that draws from a secure random source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick length over complexity.&lt;/strong&gt; Set the generator to 16 or more characters, or 5 or 6 words for anything you need to remember.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store it in a password manager&lt;/strong&gt;, and use a different password for every site so one breach doesn't expose the rest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want a quick generator, the free &lt;a href="https://pixelvalut.com/tools/password-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=password-length" rel="noopener noreferrer"&gt;PixelValut password generator&lt;/a&gt; creates random passwords, passphrases and PINs in your browser using &lt;code&gt;crypto.getRandomValues&lt;/code&gt;, shows the entropy in bits, and clears your clipboard 30 seconds after copying. Generation happens locally, so the password isn't sent to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers: store passwords properly
&lt;/h2&gt;

&lt;p&gt;Length only protects users if your app stores the result well. Hash with a slow, salted algorithm such as Argon2id or bcrypt, as the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Password Storage Cheat Sheet&lt;/a&gt; recommends, so the 10-billion-guesses-per-second figure above drops sharply. Accept long passwords and pasted passwords, and check new ones against a breached-password list.&lt;/p&gt;

&lt;p&gt;If you look after a domain as well as logins, my guides on &lt;a href="https://dev.to/muhammadalimughal/how-to-check-an-ssl-certificate-in-10-minutes-and-fix-the-most-common-https-errors-3274"&gt;checking an SSL certificate&lt;/a&gt; and &lt;a href="https://dev.to/muhammadalimughal/how-to-check-spf-dkim-and-dmarc-records-in-10-minutes-and-fix-the-common-mistakes-18dc"&gt;checking SPF, DKIM and DMARC records&lt;/a&gt; cover the other two security checks I run on every site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strong password checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;15+ characters for any password that works without 2FA&lt;/li&gt;
&lt;li&gt;Randomly generated, not based on words, names or dates&lt;/li&gt;
&lt;li&gt;5 to 6 random words if you must memorise it&lt;/li&gt;
&lt;li&gt;Unique for every account&lt;/li&gt;
&lt;li&gt;Saved in a password manager&lt;/li&gt;
&lt;li&gt;Two-factor authentication turned on where available&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How long should a password be in 2026?
&lt;/h3&gt;

&lt;p&gt;Use at least 15 random characters for any account protected by a password alone, which matches NIST SP 800-63B-4. With two-factor authentication the minimum drops to 8, but I still use 16 or more for anything stored in a password manager, because there's no extra effort once the manager remembers it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a 12-character password long enough?
&lt;/h3&gt;

&lt;p&gt;A truly random 12-character password from the full keyboard has about 79 bits of entropy, which is strong against offline guessing. The problem is that most 12-character passwords people choose aren't random. If you're creating one by hand, go longer, or use a generator and a password manager instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are passphrases better than passwords?
&lt;/h3&gt;

&lt;p&gt;A passphrase of 5 or 6 random words is easier to type and remember than a random string of similar strength. The words must be picked at random from a large list, not chosen by you. A 4-word passphrase is too weak by my numbers, so treat 5 words as the minimum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do special characters make a password stronger?
&lt;/h3&gt;

&lt;p&gt;They help a little by enlarging the character pool, but length helps far more. In my test, 15 random lowercase letters had more entropy than 8 characters using every symbol. NIST now tells services not to require symbols at all, because forced rules lead to predictable patterns like a capital first letter.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I change my password?
&lt;/h3&gt;

&lt;p&gt;Only when there's a reason, such as a breach notice, a leaked password or a shared account losing a member. NIST SP 800-63B-4 says services shall not force periodic changes. Routine resets push people toward small, predictable edits, which makes passwords weaker rather than stronger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate one now
&lt;/h2&gt;

&lt;p&gt;Pick an account that still uses an old, short password and replace it with a 16-character random one from the &lt;a href="https://pixelvalut.com/tools/password-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=password-length" rel="noopener noreferrer"&gt;password generator&lt;/a&gt;. You can also visit &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=password-length" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the longest password a website has rejected for you? Tell me in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  security #passwords #cybersecurity #privacy #infosec #webdev
&lt;/h1&gt;

</description>
      <category>security</category>
      <category>beginners</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Track Job Applications: A Simple System That Works</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:11:46 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-track-job-applications-a-simple-system-that-works-12oc</link>
      <guid>https://dev.to/muhammadalimughal/how-to-track-job-applications-a-simple-system-that-works-12oc</guid>
      <description>&lt;p&gt;&lt;em&gt;By Muhammad Ali Mughal · Last updated 24 October 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three weeks into a job search, most people can't answer simple questions about it. Which companies replied? Which resume did I send to that fintech role? Is it time to follow up, or did they already reject me? If you're applying to more than a handful of jobs, you need to know how to track job applications properly, and the fix is smaller than you think: one row per application, a short list of statuses, the exact resume file you sent, and a follow-up date.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal. I build &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=track-job-applications" rel="noopener noreferrer"&gt;Smart Resume Analyzer&lt;/a&gt;, and I've run my own searches with nothing more than a spreadsheet and a reminder habit. Below is the system I use, the columns that matter, and a small script I ran on 20 sample applications to show what the data tells you after a few weeks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F920i0q7l4idom8tdm9j3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F920i0q7l4idom8tdm9j3.png" alt="A desk monitor showing a colour-coded job application board, with a desk calendar, sticky notes and printed resumes" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tracking job applications changes your results
&lt;/h2&gt;

&lt;p&gt;Tracking isn't admin for its own sake. It answers three questions you can't answer from memory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where do my applications stall?&lt;/strong&gt; No replies at all usually points at the resume or the targeting. Replies but no interviews points at fit or timing. Interviews with no offers points at the interview itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which version of my resume works?&lt;/strong&gt; If you tailor resumes (and you should), you need to know which file went where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who needs a follow-up today?&lt;/strong&gt; A polite nudge one to two weeks after applying is normal. Without dates, it never happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Choose one place to track job applications
&lt;/h2&gt;

&lt;p&gt;Pick one tool and stick with it. Three options work well:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A spreadsheet (Excel or Google Sheets)&lt;/td&gt;
&lt;td&gt;Full control, filters, charts&lt;/td&gt;
&lt;td&gt;You must add dropdowns and reminders yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A browser-based tracker&lt;/td&gt;
&lt;td&gt;Quick setup, built-in stages&lt;/td&gt;
&lt;td&gt;Check where the data is stored and how to back it up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A notes app or email folder&lt;/td&gt;
&lt;td&gt;Very small searches&lt;/td&gt;
&lt;td&gt;Falls apart past 15 to 20 applications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you like spreadsheets, I made a free &lt;a href="https://smartresumeanalyzer.com/application-tracking-template?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=track-job-applications" rel="noopener noreferrer"&gt;application tracking template&lt;/a&gt; you can open in Excel or import into Google Sheets. If you'd rather not build anything, a browser tracker with built-in stages, notes and reminders is quicker. Just check where it stores data and export a backup now and then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Set up the columns that matter
&lt;/h2&gt;

&lt;p&gt;More columns isn't better. These are the ones I actually use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Date applied&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Company&lt;/strong&gt; and &lt;strong&gt;role title&lt;/strong&gt; (copy the exact title from the posting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link to the posting&lt;/strong&gt; plus the requisition or job ID if there is one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; referral, company site, LinkedIn, job board&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume version:&lt;/strong&gt; the exact filename you uploaded, for example &lt;code&gt;muhammad-laravel-v2.pdf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; a fixed list, not free text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last contact&lt;/strong&gt; and &lt;strong&gt;next action date&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notes:&lt;/strong&gt; recruiter name, salary range, anything said on a call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The posting link matters more than people expect. Job ads get taken down, and you'll want the original wording before an interview. Save the text or a PDF of the ad in your notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Use a fixed list of statuses
&lt;/h2&gt;

&lt;p&gt;Free-text statuses ("waiting?", "maybe", "heard back") make the sheet impossible to filter. Use a short dropdown. In Google Sheets, select the status column and add an &lt;a href="https://support.google.com/docs/answer/186103" rel="noopener noreferrer"&gt;in-cell dropdown list&lt;/a&gt;. Excel has the same feature under Data Validation.&lt;/p&gt;

&lt;p&gt;I use these stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Saved:&lt;/strong&gt; found it, not applied yet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applied&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replied:&lt;/strong&gt; any human response, including a screening email&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interview&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejected&lt;/strong&gt; or &lt;strong&gt;No reply&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add &lt;a href="https://support.google.com/docs/answer/78413" rel="noopener noreferrer"&gt;conditional formatting&lt;/a&gt; so a row turns amber when its next action date has passed. That one rule does most of the reminding for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Log the resume version for every application
&lt;/h2&gt;

&lt;p&gt;This is the column most trackers skip, and it's the one that tells you whether your edits are working. Name files by target role and version, and never overwrite an old file you've already sent.&lt;/p&gt;

&lt;p&gt;When you tailor a resume to a posting, it helps to know which ATS the company uses and how it reads your file. I covered that in &lt;a href="https://dev.to/muhammadalimughal/which-ats-is-this-job-using-how-to-tell-from-the-job-link-and-what-to-do-differently-1ldb"&gt;Which ATS Is This Job Using?&lt;/a&gt;, and the keyword side in &lt;a href="https://dev.to/muhammadalimughal/how-to-tailor-your-resume-keywords-to-a-job-description-without-keyword-stuffing-55ho"&gt;How to Tailor Your Resume Keywords to a Job Description&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Review the numbers every week
&lt;/h2&gt;

&lt;p&gt;Once you have 15 or more rows, the sheet starts to talk. I wrote a short Python script and ran it on a sample CSV of 20 made-up applications (fictional companies, realistic mix of sources and statuses) to show the kind of review I do on Sundays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;TODAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;applications_sample.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;responded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;responded&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response rate by source:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Response rate by resume version:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resume_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;due&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
       &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;TODAY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_contact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Follow-ups due (10+ days silent):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output on my machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response rate by source: {'Company site': '3/4 (75%)', 'Job board': '1/7 (14%)', 'LinkedIn': '3/5 (60%)', 'Referral': '4/4 (100%)'}
Response rate by resume version: {'backend-v1': '1/4 (25%)', 'backend-v2': '4/5 (80%)', 'dotnet-v1': '2/3 (67%)', 'laravel-v1': '1/5 (20%)', 'laravel-v2': '3/3 (100%)'}
Follow-ups due (10+ days silent): 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full script also prints totals: 20 applications, 11 responses and 5 at interview or better. The numbers are from sample data, so don't read them as a benchmark. The point is what the same view shows you on your own data. In this sample, the "v2" resumes did far better than the "v1" files, and job-board applications were mostly silent. That's a signal to retire the old files and spend more time on referrals and company career pages. It also shows nine follow-ups that would have been forgotten.&lt;/p&gt;

&lt;p&gt;No Python? A pivot table on Source and Resume version gives the same answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Follow up on a schedule
&lt;/h2&gt;

&lt;p&gt;When you log an application, set the next action date 7 to 10 days out. When that date arrives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send one short, specific follow-up to the recruiter or hiring manager&lt;/li&gt;
&lt;li&gt;Update &lt;strong&gt;Last contact&lt;/strong&gt; and push the next action date another week&lt;/li&gt;
&lt;li&gt;After two follow-ups with no reply, mark it &lt;strong&gt;No reply&lt;/strong&gt; and move on&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Job application tracking checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One tool, used for every application&lt;/li&gt;
&lt;li&gt;Posting link and job ID saved, plus a copy of the ad text&lt;/li&gt;
&lt;li&gt;Exact resume filename logged per row&lt;/li&gt;
&lt;li&gt;Status chosen from a fixed list&lt;/li&gt;
&lt;li&gt;Next action date on every open row&lt;/li&gt;
&lt;li&gt;Weekly review of response rate by source and resume version&lt;/li&gt;
&lt;li&gt;Backup or export of your data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the best way to track job applications?
&lt;/h3&gt;

&lt;p&gt;The best way is the one you'll keep using. For most people that's a spreadsheet or a simple browser tracker with one row per application, a fixed status list, the resume version sent, and a next action date. Tools matter less than logging every application on the same day you apply.&lt;/p&gt;

&lt;h3&gt;
  
  
  What columns should a job application tracker have?
&lt;/h3&gt;

&lt;p&gt;At minimum: date applied, company, role title, posting link, source, resume version, status, last contact, next action date and notes. Add the job or requisition ID when the posting shows one. Skip columns you won't fill in, because half-empty columns make the sheet harder to read and review.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many job applications should I track at once?
&lt;/h3&gt;

&lt;p&gt;Track every application, however many there are. The tracker becomes most useful after about 15 rows, because that's when patterns in sources and resume versions start to appear. If you're sending more than 10 a week, review the sheet weekly so follow-ups and stale rows don't pile up.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I follow up on a job application?
&lt;/h3&gt;

&lt;p&gt;One to two weeks after applying is a common and polite window. Set the next action date when you log the application, send one short follow-up when it arrives, and try once more a week later. After two attempts with no reply, mark the row as no reply and focus elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a spreadsheet better than a job tracker app?
&lt;/h3&gt;

&lt;p&gt;A spreadsheet gives you full control, pivot tables and charts, but you build the dropdowns and reminders yourself. A tracker app is faster to start and has stages built in. Whichever you pick, check where your data is stored and keep an export, especially if it only lives in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start tracking today
&lt;/h2&gt;

&lt;p&gt;Open the template above or the free &lt;a href="https://smartresumeanalyzer.com/tools/job-tracker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=track-job-applications" rel="noopener noreferrer"&gt;Smart Resume job tracker&lt;/a&gt;, which needs no account and keeps your data in your browser with an export option. Add every application you've sent this month, and set a next action date on each one. You can visit &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=track-job-applications" rel="noopener noreferrer"&gt;Smart Resume Analyzer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you track your applications right now? Tell me in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  jobsearch #career #resume #productivity #jobapplication #careeradvice
&lt;/h1&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Running Laravel Queues in Production: Redis, Supervisor, Retries and Failed Jobs</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 01:37:51 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/running-laravel-queues-in-production-redis-supervisor-retries-and-failed-jobs-278m</link>
      <guid>https://dev.to/muhammadalimughal/running-laravel-queues-in-production-redis-supervisor-retries-and-failed-jobs-278m</guid>
      <description>&lt;p&gt;Laravel queues feel easy in development. You set &lt;code&gt;QUEUE_CONNECTION&lt;/code&gt;, run &lt;code&gt;php artisan queue:work&lt;/code&gt; in a terminal, and emails send, PDFs generate and webhooks fire in the background. Then you deploy, close the terminal, and a week later a customer asks why their order confirmation never arrived.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and at &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-queues-in-production" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;, our Karachi team builds and maintains Laravel applications, including e-commerce stores that rely on queues for order emails, courier bookings, SMS and payment callbacks. This is the production setup we use, and the mistakes it's designed to prevent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vx6sdjht2c7f2z6hesc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vx6sdjht2c7f2z6hesc.png" alt="A server terminal and monitoring dashboard with background job workers processing messages and parcels, and a queue depth graph" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pick a real queue driver
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;sync&lt;/code&gt; driver runs jobs immediately inside the request, which is fine for tests but defeats the point in production. The &lt;code&gt;database&lt;/code&gt; driver works and is a reasonable start for small apps. For anything busy, I use &lt;strong&gt;Redis&lt;/strong&gt;: it's fast, it's what Laravel Horizon is built on, and it keeps queue traffic off your main database.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Redis also holds your cache and sessions, consider a separate Redis database or connection for queues, so clearing the cache never wipes pending jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Split work into named queues
&lt;/h2&gt;

&lt;p&gt;Not all jobs are equally urgent. A password reset email shouldn't wait behind a 5,000-row export. Dispatch onto named queues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;SendOrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'high'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;GenerateMonthlyReport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'low'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then tell the worker to process them in priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:work redis &lt;span class="nt"&gt;--queue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high,default,low
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker always empties &lt;code&gt;high&lt;/code&gt; before looking at &lt;code&gt;default&lt;/code&gt;, and &lt;code&gt;default&lt;/code&gt; before &lt;code&gt;low&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Keep workers alive with Supervisor
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;queue:work&lt;/code&gt; is a long-running process. If it crashes, runs out of memory or the server reboots, nothing restarts it unless you have a process monitor. On Ubuntu, Supervisor is the standard choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;supervisor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;/etc/supervisor/conf.d/laravel-worker.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[program:laravel-worker]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;process_name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%(program_name)s_%(process_num)02d&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;php /var/www/app/artisan queue:work redis --queue=high,default,low --sleep=3 --tries=3 --max-time=3600&lt;/span&gt;
&lt;span class="py"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;autorestart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stopasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;killasgroup&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;www-data&lt;/span&gt;
&lt;span class="py"&gt;numprocs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;redirect_stderr&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;stdout_logfile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/var/www/app/storage/logs/worker.log&lt;/span&gt;
&lt;span class="py"&gt;stopwaitsecs&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl reread
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl update
&lt;span class="nb"&gt;sudo &lt;/span&gt;supervisorctl start &lt;span class="s2"&gt;"laravel-worker:*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes on those options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;numprocs&lt;/code&gt;&lt;/strong&gt; is how many workers run in parallel. Start with two and increase only if the queue backs up; each worker holds your whole app in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--max-time=3600&lt;/code&gt;&lt;/strong&gt; makes each worker exit after an hour so Supervisor starts a fresh one. This limits slow memory leaks. &lt;code&gt;--max-jobs&lt;/code&gt; does the same by job count, and &lt;code&gt;--memory&lt;/code&gt; sets a memory ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stopwaitsecs&lt;/code&gt;&lt;/strong&gt; should be longer than your longest job, so Supervisor doesn't kill a worker mid-job during a stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;user&lt;/code&gt;&lt;/strong&gt; should match the user that owns your app files, or you'll get permission errors on logs and storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Restart workers on every deploy
&lt;/h2&gt;

&lt;p&gt;This one catches almost everyone. A running worker has already loaded your code into memory. Deploy new code and the workers keep running the &lt;strong&gt;old&lt;/strong&gt; version until they restart.&lt;/p&gt;

&lt;p&gt;Add this to the end of your deploy script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It signals workers to finish their current job and exit gracefully; Supervisor then starts them again with the new code. Clear and rebuild config cache first (&lt;code&gt;php artisan config:cache&lt;/code&gt;) so the new workers pick up the right settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Set timeouts and retries deliberately
&lt;/h2&gt;

&lt;p&gt;Three settings work together, and getting them out of sync causes jobs to run twice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job &lt;code&gt;$timeout&lt;/code&gt;:&lt;/strong&gt; the maximum seconds a job may run before the worker kills it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retry_after&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;config/queue.php&lt;/code&gt;: how long the queue waits before assuming a job died and handing it to another worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$tries&lt;/code&gt; / &lt;code&gt;$backoff&lt;/code&gt;:&lt;/strong&gt; how many attempts, and how long to wait between them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule: &lt;code&gt;retry_after&lt;/code&gt; must be longer than your longest job timeout.&lt;/strong&gt; If a job legitimately takes 120 seconds and &lt;code&gt;retry_after&lt;/code&gt; is 90, a second worker picks it up while the first is still running.&lt;/p&gt;

&lt;p&gt;On the job class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BookCourierShipment&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// seconds before retries 1, 2, 3+&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CourierClient&lt;/span&gt; &lt;span class="nv"&gt;$courier&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$courier&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Increasing backoff matters for third-party APIs. If a courier or SMS gateway is down, hammering it every three seconds won't help, and may get you rate-limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Handle failed jobs instead of losing them
&lt;/h2&gt;

&lt;p&gt;When a job runs out of attempts, Laravel records it in the &lt;code&gt;failed_jobs&lt;/code&gt; table (recent Laravel versions include that migration by default). Learn these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan queue:failed            &lt;span class="c"&gt;# list failed jobs&lt;/span&gt;
php artisan queue:retry &amp;lt;uuid&amp;gt;      &lt;span class="c"&gt;# retry one&lt;/span&gt;
php artisan queue:retry all         &lt;span class="c"&gt;# retry all&lt;/span&gt;
php artisan queue:forget &amp;lt;uuid&amp;gt;     &lt;span class="c"&gt;# delete one&lt;/span&gt;
php artisan queue:prune-failed &lt;span class="nt"&gt;--hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;168
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;failed()&lt;/code&gt; method to important jobs so someone actually finds out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Courier booking failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'order'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;?-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="c1"&gt;// notify the ops team by email or Slack&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule the prune command so the table doesn't grow forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make jobs safe to run twice
&lt;/h2&gt;

&lt;p&gt;Even with correct settings, a job can occasionally run more than once: a worker crashes after doing the work but before acknowledging it, or someone retries a failed job that half-succeeded. Design for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check before acting.&lt;/strong&gt; Before booking a shipment, check whether the order already has a tracking number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use unique jobs.&lt;/strong&gt; Implement &lt;code&gt;ShouldBeUnique&lt;/code&gt; to stop duplicate jobs for the same model from being queued at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevent overlap.&lt;/strong&gt; The &lt;code&gt;WithoutOverlapping&lt;/code&gt; job middleware stops two jobs for the same key from running simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dispatch after commit.&lt;/strong&gt; If you dispatch inside a database transaction, use &lt;code&gt;-&amp;gt;afterCommit()&lt;/code&gt; (or set &lt;code&gt;after_commit&lt;/code&gt; on the connection) so the worker doesn't look for a record that isn't saved yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters most for payment callbacks and inventory updates, where a duplicate means a double charge or a wrong stock count.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Watch the queue
&lt;/h2&gt;

&lt;p&gt;You can't fix what you can't see. Options, from simplest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tail &lt;code&gt;storage/logs/worker.log&lt;/code&gt; and check &lt;code&gt;supervisorctl status&lt;/code&gt; after deploys.&lt;/li&gt;
&lt;li&gt;Schedule &lt;code&gt;php artisan queue:monitor redis:high,redis:default --max=100&lt;/code&gt; so Laravel fires an event when a queue grows past a threshold, and send yourself an alert from it.&lt;/li&gt;
&lt;li&gt;Install &lt;strong&gt;Laravel Horizon&lt;/strong&gt; if you're on Redis. It gives you a dashboard of throughput, wait times and failed jobs, and manages worker processes from a config file. With Horizon, Supervisor runs &lt;code&gt;php artisan horizon&lt;/code&gt; instead of &lt;code&gt;queue:work&lt;/code&gt;, and you restart with &lt;code&gt;php artisan horizon:terminate&lt;/code&gt; on deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redis (or database) driver, never &lt;code&gt;sync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Named queues with priority order&lt;/li&gt;
&lt;li&gt;Supervisor running workers with &lt;code&gt;--max-time&lt;/code&gt; or &lt;code&gt;--max-jobs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queue:restart&lt;/code&gt; (or &lt;code&gt;horizon:terminate&lt;/code&gt;) in every deploy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry_after&lt;/code&gt; longer than the longest job &lt;code&gt;$timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$tries&lt;/code&gt; and increasing &lt;code&gt;backoff&lt;/code&gt; on jobs that call external APIs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed()&lt;/code&gt; handlers and a scheduled prune&lt;/li&gt;
&lt;li&gt;Idempotent jobs for payments, stock and shipments&lt;/li&gt;
&lt;li&gt;Queue-length alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like a team to build or review this for your app, our &lt;a href="https://looporigin.com/services/laravel-development?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-queues-in-production" rel="noopener noreferrer"&gt;Laravel development services&lt;/a&gt; cover queued jobs, integrations and deployment. You can see our other work at &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=laravel-queues-in-production" rel="noopener noreferrer"&gt;looporigin.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the worst queue bug you've hit in production? Tell me in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  laravel #php #redis #webdev #backend #devops #queues #supervisor #ubuntu #webdevelopment #pakistan #karachi
&lt;/h1&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Check an SSL Certificate in 10 Minutes (and Fix the Most Common HTTPS Errors)</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 01:37:10 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-check-an-ssl-certificate-in-10-minutes-and-fix-the-most-common-https-errors-3274</link>
      <guid>https://dev.to/muhammadalimughal/how-to-check-an-ssl-certificate-in-10-minutes-and-fix-the-most-common-https-errors-3274</guid>
      <description>&lt;p&gt;An expired SSL certificate is one of the most avoidable outages on the web. The site is fine, the server is fine, but every visitor gets a full-page browser warning and most of them leave. Less dramatic certificate problems, like a missing intermediate certificate, are worse in a way: the site works in your desktop browser, so you don't notice, while API clients, mobile apps and &lt;code&gt;curl&lt;/code&gt; quietly fail.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I build free developer and privacy tools at &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-ssl-certificate-10-minutes" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;. This is the checklist I run whenever I launch a site, move a server or get a "your connection is not private" report. It takes about ten minutes, and I've included the OpenSSL commands so you can verify everything yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1852pp18d3by1k1l0ou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd1852pp18d3by1k1l0ou.png" alt="A monitor showing a website security dashboard with certificate details, a broken chain link being repaired and a list of security header toggles" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a certificate check should cover
&lt;/h2&gt;

&lt;p&gt;A useful check answers five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Is the certificate valid right now, and when does it expire?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it cover the exact hostname&lt;/strong&gt; visitors use, including &lt;code&gt;www&lt;/code&gt; and any subdomains?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the chain complete&lt;/strong&gt;, so every client can link it to a trusted root?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the server configured sensibly:&lt;/strong&gt; modern TLS versions, HTTP redirecting to HTTPS?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are the important security headers set&lt;/strong&gt;, such as HSTS?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The quickest way to get the overview is the free &lt;a href="https://pixelvalut.com/tools/ssl-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-ssl-certificate-10-minutes" rel="noopener noreferrer"&gt;SSL Checker on PixelValut&lt;/a&gt;. Enter a domain and it shows the issuer, an expiry countdown and the HTTP security headers the site returns, with fix tips and OpenSSL commands. Then use the commands below to dig into anything that looks wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check expiry and issuer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see &lt;code&gt;notBefore&lt;/code&gt; and &lt;code&gt;notAfter&lt;/code&gt;. If &lt;code&gt;notAfter&lt;/code&gt; is in the past, browsers show &lt;code&gt;NET::ERR_CERT_DATE_INVALID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-servername&lt;/code&gt; flag matters. It sends SNI (Server Name Indication), which tells a server hosting many sites which certificate to present. Without it, you may be looking at the server's default certificate rather than the one your visitors get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real fix is automated renewal.&lt;/strong&gt; Free certificates from Let's Encrypt are short-lived by design, so a manual renewal process will eventually be forgotten. If you use Certbot, test renewal without waiting for it to fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common reasons renewal breaks: port 80 is blocked by a firewall (the HTTP-01 challenge needs it), a redirect rule interferes with &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;, or the DNS now points to a different server. Also confirm the web server reloads after renewal; a renewed file on disk doesn't help if Nginx is still serving the old one from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check the hostname matches
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-ext&lt;/span&gt; subjectAltName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browsers check the &lt;strong&gt;Subject Alternative Name (SAN)&lt;/strong&gt; list, not just the common name. If visitors use &lt;code&gt;www.example.com&lt;/code&gt; and the SAN list only has &lt;code&gt;example.com&lt;/code&gt;, they get &lt;code&gt;NET::ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Things to remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A wildcard like &lt;code&gt;*.example.com&lt;/code&gt; covers &lt;code&gt;shop.example.com&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;example.com&lt;/code&gt; itself, and not &lt;code&gt;a.b.example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you add a new subdomain, reissue the certificate to include it, or use a wildcard via DNS validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Check the chain is complete
&lt;/h2&gt;

&lt;p&gt;This is the error that hides from you. Your server should send its own certificate &lt;strong&gt;plus the intermediate certificate(s)&lt;/strong&gt;. Desktop browsers can often fill a missing intermediate themselves, so the site looks fine to you. Many other clients can't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-showcerts&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^ *[0-9] s:|Verify return code"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see more than one certificate in the list and &lt;code&gt;Verify return code: 0 (ok)&lt;/code&gt;. A result like &lt;code&gt;unable to get local issuer certificate&lt;/code&gt; or &lt;code&gt;unable to verify the first certificate&lt;/code&gt; usually means the intermediate is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; point your server at the full chain file, not just the leaf certificate. With Certbot on Nginx that's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/letsencrypt/live/example.com/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/example.com/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;cert.pem&lt;/code&gt; instead of &lt;code&gt;fullchain.pem&lt;/code&gt; is one of the most common mistakes I see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Check TLS versions and redirects
&lt;/h2&gt;

&lt;p&gt;Modern clients expect TLS 1.2 or 1.3. You can confirm what the server accepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-tls1_3&lt;/span&gt; &amp;lt;/dev/null
openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-tls1_1&lt;/span&gt; &amp;lt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first should connect. The second should fail on a well-configured server (assuming your OpenSSL build still supports TLS 1.1 for testing). In Nginx, &lt;code&gt;ssl_protocols TLSv1.2 TLSv1.3;&lt;/code&gt; is a sensible baseline.&lt;/p&gt;

&lt;p&gt;Then confirm plain HTTP redirects to HTTPS with a single permanent redirect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; http://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^HTTP|^location"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want a &lt;code&gt;301&lt;/code&gt; (or &lt;code&gt;308&lt;/code&gt;) pointing to the &lt;code&gt;https://&lt;/code&gt; URL. Long redirect chains (http → https → www → trailing slash) slow down the first visit and are worth flattening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Check security headers
&lt;/h2&gt;

&lt;p&gt;A valid certificate secures the connection. Headers tell the browser how to behave on top of it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forces HTTPS on future visits&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-age=31536000; includeSubDomains&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Content-Security-Policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Limits where scripts, styles and frames can load from&lt;/td&gt;
&lt;td&gt;Start with a report-only policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X-Content-Type-Options&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops MIME-type sniffing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;X-Frame-Options&lt;/code&gt; or CSP &lt;code&gt;frame-ancestors&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Prevents clickjacking via framing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DENY&lt;/code&gt; or &lt;code&gt;SAMEORIGIN&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Referrer-Policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls how much URL data leaks to other sites&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"strict-transport|content-security|x-content-type|x-frame|referrer-policy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A word of caution on HSTS: once browsers cache it, they refuse plain HTTP for that domain until &lt;code&gt;max-age&lt;/code&gt; runs out. Start with a short &lt;code&gt;max-age&lt;/code&gt; (a few minutes or a day), confirm everything, including subdomains, works over HTTPS, and only then raise it. Only add &lt;code&gt;includeSubDomains&lt;/code&gt; when every subdomain has a valid certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Fix mixed content
&lt;/h2&gt;

&lt;p&gt;If the padlock shows a warning even though the certificate is fine, the page is probably loading something over &lt;code&gt;http://&lt;/code&gt;, like an image, script or font. Open your browser's developer console; mixed content warnings list the exact URLs. Update them to &lt;code&gt;https://&lt;/code&gt; or relative paths, and check hard-coded URLs in your database or CMS settings after a migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common errors at a glance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ERR_CERT_DATE_INVALID&lt;/code&gt;:&lt;/strong&gt; expired (or the visitor's clock is wrong). Renew and automate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ERR_CERT_COMMON_NAME_INVALID&lt;/code&gt;:&lt;/strong&gt; hostname missing from SAN. Reissue with every hostname.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unable to get local issuer certificate&lt;/code&gt;:&lt;/strong&gt; incomplete chain. Serve &lt;code&gt;fullchain.pem&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;/code&gt;:&lt;/strong&gt; self-signed or untrusted issuer. Use a publicly trusted CA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong certificate on a shared server:&lt;/strong&gt; SNI or virtual host misconfiguration. Check &lt;code&gt;server_name&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Padlock warning, valid cert:&lt;/strong&gt; mixed content. Fix the &lt;code&gt;http://&lt;/code&gt; asset URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make it routine
&lt;/h2&gt;

&lt;p&gt;My habit: check every domain after any server change, and keep an eye on expiry dates monthly even with auto-renewal, because automation fails silently. If DNS changes are involved, the &lt;a href="https://pixelvalut.com/tools/dns-lookup?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-ssl-certificate-10-minutes" rel="noopener noreferrer"&gt;DNS Lookup tool&lt;/a&gt; helps confirm the domain points where you expect before you debug certificates.&lt;/p&gt;

&lt;p&gt;Run your own domain through the &lt;a href="https://pixelvalut.com/tools/ssl-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-ssl-certificate-10-minutes" rel="noopener noreferrer"&gt;SSL Checker&lt;/a&gt; now and see what comes back. The other free tools are on &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-ssl-certificate-10-minutes" rel="noopener noreferrer"&gt;pixelvalut.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the strangest SSL issue you've had to debug? Share it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  ssl #https #security #webdev #devops #tls #cybersecurity #sysadmin #nginx #webdevelopment #privacy #infosec
&lt;/h1&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Which ATS Is This Job Using? How to Tell From the Job Link (and What to Do Differently)</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Sat, 24 Oct 2026 01:36:35 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/which-ats-is-this-job-using-how-to-tell-from-the-job-link-and-what-to-do-differently-1ldb</link>
      <guid>https://dev.to/muhammadalimughal/which-ats-is-this-job-using-how-to-tell-from-the-job-link-and-what-to-do-differently-1ldb</guid>
      <description>&lt;p&gt;You found a job you want, clicked Apply, and landed on a form you've never seen before. Maybe it wants you to create an account. Maybe it fills in your work history from your resume and gets half of it wrong. Maybe it's a single page with an upload button and nothing else.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I built &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;Smart Resume Analyzer&lt;/a&gt;, a free AI resume and ATS checker. One question I see a lot is "which ATS is this company using, and does it change what I should do?" The short answer: you can usually tell from the job link in a few seconds, and while it doesn't change what a good resume looks like, it does change how you should handle the application. Here's how I check, and what I do differently for each type of system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2p76hv1q26r7n3jcztqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2p76hv1q26r7n3jcztqa.png" alt="A laptop showing an online job application form auto-filled from a resume, with fields being checked and corrected" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother knowing the system?
&lt;/h2&gt;

&lt;p&gt;An applicant tracking system (ATS) is the software a company uses to post jobs, collect applications and move candidates through hiring stages. Knowing which one sits behind a posting helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expect the right process.&lt;/strong&gt; Some systems ask you to create an account per employer; others take an upload and an email address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch parsing mistakes.&lt;/strong&gt; Systems that pre-fill a profile from your resume show you exactly what they extracted. That's your chance to fix it before a recruiter sees it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan your time.&lt;/strong&gt; A long multi-page form with screening questions takes 20 to 40 minutes. A short form takes five. Knowing which you're facing helps you batch applications sensibly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it should &lt;em&gt;not&lt;/em&gt; do is make you build a different resume for every vendor. The fundamentals work everywhere: one column, standard headings, real text (not images), and wording that matches the role.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Read the job link
&lt;/h2&gt;

&lt;p&gt;Most ATS vendors host job pages on their own domains. Look at the address bar on the job page or, better, on the page you reach after clicking &lt;strong&gt;Apply&lt;/strong&gt;. Common patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If the link contains...&lt;/th&gt;
&lt;th&gt;It's usually...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;greenhouse.io&lt;/code&gt; (e.g. &lt;code&gt;boards.greenhouse.io&lt;/code&gt;, &lt;code&gt;job-boards.greenhouse.io&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.lever.co&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;myworkdayjobs.com&lt;/code&gt; or &lt;code&gt;myworkdaysite.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;icims.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iCIMS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;taleo.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Oracle Taleo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.ashbyhq.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smartrecruiters.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apply.workable.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bamboohr.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BambooHR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobvite.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jobvite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;recruitee.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;teamtailor.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Teamtailor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;successfactors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SAP SuccessFactors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;oraclecloud.com&lt;/code&gt; with &lt;code&gt;CandidateExperience&lt;/code&gt; in the path&lt;/td&gt;
&lt;td&gt;Oracle Recruiting Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't want to memorise that table, I made a free tool for it: &lt;a href="https://smartresumeanalyzer.com/which-ats-is-this-job-using?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;Which ATS Is This Job Using?&lt;/a&gt;. You paste the job link and it compares the host name and a few known address patterns against fifteen common systems. The check runs in your browser, nothing is fetched from the job page, and if it can't tell, it says "unknown" instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: If the link only shows the company, follow the Apply button
&lt;/h2&gt;

&lt;p&gt;Large employers often run their careers site on their own domain (&lt;code&gt;careers.company.com&lt;/code&gt;) and load the ATS inside it. The address then shows only the employer. Two quick ways to find the vendor anyway:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Click Apply and look again.&lt;/strong&gt; The application step frequently moves you to the vendor's domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the page footer and privacy links.&lt;/strong&gt; Many hosted job boards include a small "Powered by" line or link to the vendor's privacy notice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you still can't tell, that's fine. Apply with your standard ATS-friendly resume and treat it as an unknown system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Adjust how you apply, not what you are
&lt;/h2&gt;

&lt;p&gt;Here's how I approach the main families of systems. These are patterns I've seen, not promises about any single employer's configuration, because companies customise their setups heavily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Profile-builder systems (Workday, Taleo, SuccessFactors, Oracle)
&lt;/h3&gt;

&lt;p&gt;These usually ask you to create an account, upload your resume, and then review a long form that has been pre-filled from it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check every pre-filled field.&lt;/strong&gt; Dates, job titles and employers are commonly split or merged incorrectly. Fix them by hand. Recruiters often see the structured fields, not just your file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use standard section headings&lt;/strong&gt; like "Work Experience," "Education" and "Skills" so the parser maps sections correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write dates consistently&lt;/strong&gt;, for example "Mar 2022 – Present" throughout. Mixed formats are a common cause of scrambled history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a password manager entry per employer.&lt;/strong&gt; Many of these portals create a separate account for each company, and you'll need it to check your status later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget time for screening questions.&lt;/strong&gt; Answer them carefully; knockout questions (work authorisation, location, required certifications) can filter you out before a human looks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote a dedicated &lt;a href="https://smartresumeanalyzer.com/workday-resume-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;Workday resume checker page&lt;/a&gt; because this is where I see the most parsing frustration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload-and-go systems (Greenhouse, Lever, Ashby, Workable)
&lt;/h3&gt;

&lt;p&gt;These tend to have shorter forms: name, email, resume upload, links, and a few custom questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your resume file does most of the work.&lt;/strong&gt; There's less structured data to correct, so the file itself needs to read cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fill in the optional links.&lt;/strong&gt; LinkedIn, GitHub or portfolio fields are quick wins for technical roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat custom questions like a mini cover letter.&lt;/strong&gt; "Why this role?" answered in three specific sentences beats a pasted generic paragraph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirror the posting's wording&lt;/strong&gt; for skills and titles, because recruiters often search and filter candidates by keyword later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Small-business systems (BambooHR, Recruitee, Teamtailor, Breezy)
&lt;/h3&gt;

&lt;p&gt;Often used by smaller companies where the hiring manager reads applications directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead with relevance.&lt;/strong&gt; Your summary and first bullets should speak to this exact role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A short, tailored cover note matters more here&lt;/strong&gt;, because a person is likely reading it soon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Test how your resume parses before you apply
&lt;/h2&gt;

&lt;p&gt;Whatever the system, the most useful thing you can do is see your resume the way software sees it. Things that commonly break parsing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two-column layouts where the reading order gets mixed up&lt;/li&gt;
&lt;li&gt;Contact details placed in the document header or footer&lt;/li&gt;
&lt;li&gt;Skills shown as icons, progress bars or images&lt;/li&gt;
&lt;li&gt;Tables used to lay out the whole page&lt;/li&gt;
&lt;li&gt;Unusual section names like "My Journey" instead of "Experience"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run your file through an &lt;a href="https://smartresumeanalyzer.com/ats-parser-online?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;ATS parser&lt;/a&gt; and read the plain-text output. If your job titles, dates and employers come out in the right order, you're in good shape for nearly every system on the list above.&lt;/p&gt;

&lt;h2&gt;
  
  
  My quick pre-apply routine
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Paste the job link into the ATS detector.&lt;/li&gt;
&lt;li&gt;If it's a profile-builder system, set aside extra time to review every pre-filled field.&lt;/li&gt;
&lt;li&gt;Check the resume's parsed text.&lt;/li&gt;
&lt;li&gt;Compare the resume against the job description for missing keywords.&lt;/li&gt;
&lt;li&gt;Apply, then log the company, role, system and date so I can follow up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try it on the next job you're about to apply for: paste the link into the &lt;a href="https://smartresumeanalyzer.com/which-ats-is-this-job-using?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;ATS detector&lt;/a&gt;, then run your resume through the free checker on &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=which-ats-is-this-job-using" rel="noopener noreferrer"&gt;smartresumeanalyzer.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which application system frustrates you most? Tell me in the comments and I'll cover it in more detail.&lt;/p&gt;

&lt;h1&gt;
  
  
  resume #jobsearch #ats #careeradvice #resumetips #jobhunting #hiring #careers #workday #techjobs #career #productivity
&lt;/h1&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>resume</category>
      <category>jobsearch</category>
    </item>
    <item>
      <title>Planning a VoIP Phone System for a Small Business: An Asterisk and FreePBX Checklist</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:56:52 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/planning-a-voip-phone-system-for-a-small-business-an-asterisk-and-freepbx-checklist-5cn7</link>
      <guid>https://dev.to/muhammadalimughal/planning-a-voip-phone-system-for-a-small-business-an-asterisk-and-freepbx-checklist-5cn7</guid>
      <description>&lt;p&gt;When a small business asks me for a "phone system," the first conversation is rarely about phones. It's about missed calls at lunchtime, a sales team that works from home two days a week, and an owner who has no idea how many customers hung up while on hold last month.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and at &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voip-phone-system-checklist" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;, our Karachi-based team builds web applications and also designs and deploys VoIP phone systems on Asterisk and FreePBX. The single biggest lesson from those projects: &lt;strong&gt;a phone system succeeds or fails on planning, not on the software.&lt;/strong&gt; Asterisk can do almost anything. The question is what &lt;em&gt;your&lt;/em&gt; business actually needs it to do.&lt;/p&gt;

&lt;p&gt;This is the planning checklist I walk through with clients before anyone installs a server. You can use it whether you hire someone, go with a hosted cloud PBX, or build it yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz5mifsvsm6yjlx3hsqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwz5mifsvsm6yjlx3hsqs.png" alt="A call flow diagram: incoming call, IVR menu options, business hours check, queues and voicemail" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, a quick map of the options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asterisk&lt;/strong&gt; is the open-source telephony engine underneath many PBX systems. It's extremely flexible, configured through text files and dialplan logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FreePBX&lt;/strong&gt; puts a web interface on top of Asterisk, so day-to-day changes (adding an extension, editing business hours) don't need the command line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted cloud PBX&lt;/strong&gt; services run the system for you. Less control and customisation, but less to maintain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most small offices, FreePBX on Asterisk hits a good balance: a GUI for routine changes and the full power of Asterisk when you need custom behaviour. If you need deep integration with your own software, raw Asterisk dialplan or its APIs become more attractive. We compared the two in more detail in a separate post on &lt;a href="https://looporigin.com/blog/asterisk-vs-freepbx-pakistan-contact-centre?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voip-phone-system-checklist" rel="noopener noreferrer"&gt;Asterisk vs FreePBX&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Draw your call flow on paper
&lt;/h2&gt;

&lt;p&gt;Before talking about hardware, answer one question: &lt;strong&gt;what should happen when someone calls your main number?&lt;/strong&gt; Sketch it as a tree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call arrives. Is it within business hours?&lt;/li&gt;
&lt;li&gt;If yes, play a short greeting and an IVR menu: "Press 1 for sales, 2 for support, 3 for accounts."&lt;/li&gt;
&lt;li&gt;Each option goes to a &lt;strong&gt;ring group&lt;/strong&gt; (all phones ring at once) or a &lt;strong&gt;queue&lt;/strong&gt; (callers wait in line for the next free agent).&lt;/li&gt;
&lt;li&gt;If nobody answers within a set time, what then? Voicemail? Overflow to a mobile? Back to the main menu?&lt;/li&gt;
&lt;li&gt;Outside business hours, play a closed message and offer voicemail, or route urgent calls to an on-call number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep the IVR short. Three or four options is plenty, and the most common reason for calling should be option 1. Every extra menu level loses callers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Count calls, not just people
&lt;/h2&gt;

&lt;p&gt;People size phone systems by headcount ("we have 12 staff"). What actually matters is &lt;strong&gt;concurrent calls&lt;/strong&gt;: how many conversations happen at the same moment, at your busiest time.&lt;/p&gt;

&lt;p&gt;A 12-person office where only four people are ever on the phone together needs capacity for roughly four to six simultaneous calls, not twelve. This number drives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many channels you need from your SIP trunk provider&lt;/li&gt;
&lt;li&gt;Your internet bandwidth requirements&lt;/li&gt;
&lt;li&gt;The server size (though a small office PBX needs surprisingly modest hardware)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have an existing system or mobile bills, check last month's busiest hour. That's a better guide than any rule of thumb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Choose how calls reach you (SIP trunks)
&lt;/h2&gt;

&lt;p&gt;A PBX handles calls inside your business; a &lt;strong&gt;SIP trunk&lt;/strong&gt; connects it to the outside phone network. When choosing a provider, confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number availability for the cities or countries you need&lt;/li&gt;
&lt;li&gt;How many concurrent channels you get&lt;/li&gt;
&lt;li&gt;Calling rates for the destinations you actually call&lt;/li&gt;
&lt;li&gt;Whether you can keep (port) your existing numbers&lt;/li&gt;
&lt;li&gt;Support hours and how outages are handled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Carrier charges are separate from the cost of setting up the PBX itself, so get those quotes independently. Rules around telecom services, call recording and outbound calling differ by country, so check what applies to you with your provider before you go live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Decide where people will answer
&lt;/h2&gt;

&lt;p&gt;Modern PBX setups don't tie anyone to a desk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desk IP phones&lt;/strong&gt; for reception and anyone on the phone all day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Softphones&lt;/strong&gt; on laptops (Zoiper and similar apps)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile softphone apps&lt;/strong&gt; for staff on the move&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebRTC browser phones&lt;/strong&gt;, where agents answer inside a web page with a headset, with no installation needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For remote agents, plan for NAT and firewall issues early. Most "one-way audio" complaints come down to network configuration. Using a VPN or properly configured STUN/TURN for WebRTC saves a lot of frustration later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Plan integrations from day one
&lt;/h2&gt;

&lt;p&gt;This is where a custom setup earns its keep. Ask what you'd like the phone system to &lt;em&gt;know&lt;/em&gt; about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Click-to-call&lt;/strong&gt; from your CRM or admin panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen-pop:&lt;/strong&gt; when a call comes in, show the customer's record before anyone answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic call logging&lt;/strong&gt; against the customer, with duration and outcome&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; so your app can react to events (missed call: create a follow-up task)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call recordings&lt;/strong&gt; linked to the customer record, with clear retention rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because we build Laravel applications as well as PBX systems, this is the part I enjoy most. A missed call that automatically creates a follow-up task in your CRM is worth more than any fancy IVR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Don't skip reliability and security
&lt;/h2&gt;

&lt;p&gt;A PBX connected to the internet is a target. Toll fraud, where attackers make expensive international calls through your system, is a real risk. Minimum precautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong, unique SIP passwords; never use extension numbers as passwords&lt;/li&gt;
&lt;li&gt;Fail2ban or similar blocking of repeated failed registrations&lt;/li&gt;
&lt;li&gt;Firewall rules that allow SIP only from your trunk provider and known locations&lt;/li&gt;
&lt;li&gt;International calling restricted to the extensions that need it&lt;/li&gt;
&lt;li&gt;Regular updates and configuration backups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For reliability, decide what happens if your internet or server goes down. A common approach is a &lt;strong&gt;failover route&lt;/strong&gt; that forwards your main number to mobiles during an outage. Test it before you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Measure after go-live
&lt;/h2&gt;

&lt;p&gt;The point of all this is better customer service, so measure it. Useful numbers from call logs and queue reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missed and abandoned calls&lt;/strong&gt; by hour of day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average wait time&lt;/strong&gt; in each queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calls per agent&lt;/strong&gt; and average call duration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After-hours call volume&lt;/strong&gt; (often a surprise)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Review these after the first month. You'll almost always tweak something: a ring timeout that's too long, a menu option nobody uses, or a lunchtime gap that needs cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  A planning checklist you can copy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Call flow drawn for business hours and after hours&lt;/li&gt;
&lt;li&gt;IVR limited to 3-4 options, most common first&lt;/li&gt;
&lt;li&gt;Peak concurrent calls estimated&lt;/li&gt;
&lt;li&gt;SIP trunk provider chosen; numbers, channels and porting confirmed&lt;/li&gt;
&lt;li&gt;Answering devices decided: desk phones, softphones, WebRTC&lt;/li&gt;
&lt;li&gt;CRM or web app integrations listed&lt;/li&gt;
&lt;li&gt;Security basics in place: passwords, firewall, fail2ban, calling restrictions&lt;/li&gt;
&lt;li&gt;Outage failover planned and tested&lt;/li&gt;
&lt;li&gt;Reporting reviewed after 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;If you're planning a business phone system, start with Step 1 on paper this week. It costs nothing and makes every later conversation easier.&lt;/p&gt;

&lt;p&gt;If you'd like help scoping or deploying it, our &lt;a href="https://looporigin.com/services/voip-pbx-solutions?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voip-phone-system-checklist" rel="noopener noreferrer"&gt;VoIP &amp;amp; PBX solutions&lt;/a&gt; page explains what we set up: IVR and call routing, outbound dialers, Asterisk and FreePBX deployments, WebRTC remote agents, and CRM integration. You can see the rest of our work at &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=voip-phone-system-checklist" rel="noopener noreferrer"&gt;looporigin.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the biggest phone headache in your business right now: missed calls, remote staff, or zero visibility? Let me know in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  voip #asterisk #freepbx #smallbusiness #pbx #callcenter #webrtc #telecom #sip #businessphone #laravel #pakistan #karachi
&lt;/h1&gt;

</description>
      <category>voip</category>
      <category>asterisk</category>
      <category>smallbusiness</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to Check SPF, DKIM and DMARC Records in 10 Minutes (and Fix the Common Mistakes)</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:55:57 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-check-spf-dkim-and-dmarc-records-in-10-minutes-and-fix-the-common-mistakes-18dc</link>
      <guid>https://dev.to/muhammadalimughal/how-to-check-spf-dkim-and-dmarc-records-in-10-minutes-and-fix-the-common-mistakes-18dc</guid>
      <description>&lt;p&gt;Here is a deliverability problem I see a lot: a business says its invoices "just stopped arriving," and nothing changed on their side. When you look at their DNS, you find two SPF records, a DKIM key that was never published, and no DMARC record at all. The mail isn't broken. It just can't prove it's theirs, so receiving servers stop trusting it.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I built &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;, a set of free developer and network tools that includes DNS, MX, SPF, DKIM and DMARC checkers. I use them for exactly this kind of debugging. This guide walks through how to check a domain's email authentication records yourself in about ten minutes, what each record should look like, and the mistakes I see most often.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7nd1klext8725adclzc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7nd1klext8725adclzc.png" alt="An email passing three DNS-backed checkpoints between a sending and receiving mail server" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second version
&lt;/h2&gt;

&lt;p&gt;Three DNS records work together to prove an email really came from your domain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SPF&lt;/strong&gt; lists which servers are allowed to send mail for your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM&lt;/strong&gt; adds a cryptographic signature to each message; the public key lives in DNS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC&lt;/strong&gt; tells receivers what to do when SPF or DKIM fail, and where to send reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are TXT records. You don't need special access to check them. Anyone can look them up, which is also why attackers check them before spoofing a domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find out who sends mail for you
&lt;/h2&gt;

&lt;p&gt;Before checking records, make a list of everything that sends email as your domain. People usually forget half of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your mailbox provider (Google Workspace, Microsoft 365, Zoho, etc.)&lt;/li&gt;
&lt;li&gt;Your website or app (Laravel, WordPress, a contact form plugin)&lt;/li&gt;
&lt;li&gt;Transactional email services (password resets, receipts)&lt;/li&gt;
&lt;li&gt;Marketing and newsletter tools&lt;/li&gt;
&lt;li&gt;Helpdesk, CRM and invoicing software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these needs to be covered by SPF, DKIM or ideally both. Missing one is the most common reason legitimate mail fails DMARC later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Check your MX and TXT records
&lt;/h2&gt;

&lt;p&gt;Start with a general &lt;a href="https://pixelvalut.com/tools/dns-lookup?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;DNS lookup&lt;/a&gt; on your root domain and select &lt;strong&gt;MX&lt;/strong&gt; and &lt;strong&gt;TXT&lt;/strong&gt;. The MX records tell you which provider receives your mail, which is a good hint about who should also be in your SPF record.&lt;/p&gt;

&lt;p&gt;In the TXT results, look for a line starting with &lt;code&gt;v=spf1&lt;/code&gt;. That's your SPF record. You'll often see other TXT records too (domain verification tokens and so on). Those are fine; only the &lt;code&gt;v=spf1&lt;/code&gt; one matters here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Validate SPF
&lt;/h2&gt;

&lt;p&gt;A healthy SPF record looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.example-mail.com include:mailer.example.net -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your domain through the &lt;a href="https://pixelvalut.com/tools/spf-record-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;SPF record checker&lt;/a&gt;. It follows the &lt;code&gt;include:&lt;/code&gt; and &lt;code&gt;redirect=&lt;/code&gt; chains and counts DNS lookups. Here's what to look for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only one SPF record.&lt;/strong&gt; If you have two TXT records that start with &lt;code&gt;v=spf1&lt;/code&gt;, SPF evaluation returns an error. Merge them into a single record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten DNS lookups or fewer.&lt;/strong&gt; The SPF spec caps evaluation at 10 DNS-querying mechanisms (&lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt;, &lt;code&gt;redirect&lt;/code&gt;, including nested ones). Each third-party service you add can bring several lookups with it. Go over 10 and receivers return a permanent error, which usually means SPF fails. Remove services you no longer use, and avoid &lt;code&gt;ptr&lt;/code&gt; entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sensible ending.&lt;/strong&gt; &lt;code&gt;-all&lt;/code&gt; means "anything not listed should fail." &lt;code&gt;~all&lt;/code&gt; means "soft fail." &lt;code&gt;?all&lt;/code&gt; is neutral and &lt;code&gt;+all&lt;/code&gt; lets the whole internet send as you. Never use &lt;code&gt;+all&lt;/code&gt;. If you're unsure everything is listed, start with &lt;code&gt;~all&lt;/code&gt; and tighten later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every sender covered.&lt;/strong&gt; Compare the includes against your list from Step 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Check DKIM
&lt;/h2&gt;

&lt;p&gt;DKIM is trickier to inspect because the record lives at a &lt;strong&gt;selector&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selector._domainkey.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selector is chosen by whoever signs your mail. To find it, open a message you sent, view the original or full headers, and find the &lt;code&gt;DKIM-Signature&lt;/code&gt; header. The &lt;code&gt;s=&lt;/code&gt; value is the selector and &lt;code&gt;d=&lt;/code&gt; is the signing domain.&lt;/p&gt;

&lt;p&gt;Then put the selector and domain into the &lt;a href="https://pixelvalut.com/tools/dkim-record-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;DKIM record checker&lt;/a&gt;. You want to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A record that exists and starts with &lt;code&gt;v=DKIM1&lt;/code&gt; (the version tag is optional but common)&lt;/li&gt;
&lt;li&gt;A public key in the &lt;code&gt;p=&lt;/code&gt; tag. An empty &lt;code&gt;p=&lt;/code&gt; means the key has been revoked.&lt;/li&gt;
&lt;li&gt;A reasonable key size. 2048-bit RSA is a good baseline; 1024-bit keys are increasingly considered weak.&lt;/li&gt;
&lt;li&gt;No leftover &lt;code&gt;t=y&lt;/code&gt; testing flag if you've finished setting things up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each sending service usually has its own selector, so check each one. If a service offers DKIM signing with your domain and you haven't enabled it, that's worth fixing. DKIM survives forwarding better than SPF does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Read your DMARC policy
&lt;/h2&gt;

&lt;p&gt;DMARC lives at a fixed location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;_dmarc.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A typical starting record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; adkim=r; aspf=r
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it with the &lt;a href="https://pixelvalut.com/tools/dmarc-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;DMARC checker&lt;/a&gt;, which reads the policy tag by tag. The key tags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;p=&lt;/code&gt;&lt;/strong&gt; is the policy: &lt;code&gt;none&lt;/code&gt; (monitor only), &lt;code&gt;quarantine&lt;/code&gt; (send to spam), or &lt;code&gt;reject&lt;/code&gt; (block).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;rua=&lt;/code&gt;&lt;/strong&gt; is where aggregate reports go. Without it you're flying blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sp=&lt;/code&gt;&lt;/strong&gt; sets a policy for subdomains if it should differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;adkim=&lt;/code&gt; / &lt;code&gt;aspf=&lt;/code&gt;&lt;/strong&gt; control alignment: relaxed (&lt;code&gt;r&lt;/code&gt;) or strict (&lt;code&gt;s&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pct=&lt;/code&gt;&lt;/strong&gt; applies the policy to a percentage of failing mail during rollout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The part people miss: alignment
&lt;/h3&gt;

&lt;p&gt;DMARC doesn't just ask "did SPF or DKIM pass?" It asks whether they passed &lt;strong&gt;for the same domain as the visible From address.&lt;/strong&gt; If your newsletter tool passes SPF for its own bounce domain but doesn't sign with DKIM for your domain, the message can pass SPF and still fail DMARC. That's why enabling custom-domain DKIM on every service matters so much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Roll out DMARC safely
&lt;/h2&gt;

&lt;p&gt;Jumping straight to &lt;code&gt;p=reject&lt;/code&gt; is how companies block their own invoices. The rollout I recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;p=none&lt;/code&gt; with &lt;code&gt;rua&lt;/code&gt; reporting&lt;/strong&gt; for two to four weeks. Read the reports and find every legitimate source that fails.&lt;/li&gt;
&lt;li&gt;Fix SPF and DKIM for those sources.&lt;/li&gt;
&lt;li&gt;Move to &lt;strong&gt;&lt;code&gt;p=quarantine&lt;/code&gt;&lt;/strong&gt;, optionally with &lt;code&gt;pct=25&lt;/code&gt;, then raise it.&lt;/li&gt;
&lt;li&gt;When reports are clean, move to &lt;strong&gt;&lt;code&gt;p=reject&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Aggregate reports are XML files, and a single day can contain dozens of sending IPs. A DMARC report analyser helps a lot, but even skimming the source IPs and pass/fail counts will show you what you missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes I keep seeing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two SPF records&lt;/strong&gt; after adding a new tool. Always edit the existing record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too many includes&lt;/strong&gt;, pushing SPF over 10 lookups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM published on the wrong host&lt;/strong&gt;, like &lt;code&gt;selector._domainkey.yourdomain.com.yourdomain.com&lt;/code&gt; because the DNS panel already appends the domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DMARC at &lt;code&gt;p=none&lt;/code&gt; forever.&lt;/strong&gt; Monitoring is a phase, not a destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting parked domains.&lt;/strong&gt; Domains that never send mail should publish &lt;code&gt;v=spf1 -all&lt;/code&gt; and a &lt;code&gt;p=reject&lt;/code&gt; DMARC record so nobody can spoof them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not re-checking after DNS changes.&lt;/strong&gt; TTLs mean old values can linger for a while; check again after the TTL expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One SPF record, starting with &lt;code&gt;v=spf1&lt;/code&gt;, under 10 lookups, ending in &lt;code&gt;~all&lt;/code&gt; or &lt;code&gt;-all&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;DKIM enabled for every sending service, keys published and at least 2048-bit where possible&lt;/li&gt;
&lt;li&gt;DMARC record at &lt;code&gt;_dmarc&lt;/code&gt; with a &lt;code&gt;rua&lt;/code&gt; address&lt;/li&gt;
&lt;li&gt;Plan in place to move from &lt;code&gt;none&lt;/code&gt; to &lt;code&gt;quarantine&lt;/code&gt; to &lt;code&gt;reject&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Non-sending domains locked down&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Email authentication looks intimidating because it's three acronyms and a lot of semicolons. In practice it's a short checklist, and you can audit any domain with public lookups. I keep these checks on &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;pixelvalut.com&lt;/a&gt; because I needed them myself for client work, and they're free with no signup. Start with the &lt;a href="https://pixelvalut.com/tools/spf-record-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=check-spf-dkim-dmarc" rel="noopener noreferrer"&gt;SPF record checker&lt;/a&gt;, then follow the DKIM and DMARC steps above.&lt;/p&gt;

&lt;p&gt;If you want to find it again later, bookmark &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=how-to-check-spf-dkim-and-dmarc-records-in-10-minutes-and-fix-the-common-mistakes" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have you ever tracked down a deliverability problem to a DNS record? I'd love to hear the story in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  emailsecurity #dns #devops #sysadmin #spf #dkim #dmarc #cybersecurity #emaildeliverability #webdev #infosec #devtools
&lt;/h1&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>dns</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Tailor Your Resume Keywords to a Job Description (Without Keyword Stuffing)</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 20:54:57 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-tailor-your-resume-keywords-to-a-job-description-without-keyword-stuffing-55ho</link>
      <guid>https://dev.to/muhammadalimughal/how-to-tailor-your-resume-keywords-to-a-job-description-without-keyword-stuffing-55ho</guid>
      <description>&lt;p&gt;Most job seekers I talk to have one resume. They send that same file to a backend role, a full-stack role and a DevOps-flavoured role in the same afternoon, then wonder why nothing comes back.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I built &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=tailor-resume-keywords" rel="noopener noreferrer"&gt;Smart Resume Analyzer&lt;/a&gt;, a free AI resume and ATS checker. After looking at a lot of resumes next to the job posts they were sent to, the pattern is hard to miss: the resume often describes the right experience in the wrong words. A recruiter searching the ATS for "PostgreSQL" won't find you if your resume only says "databases."&lt;/p&gt;

&lt;p&gt;Tailoring fixes that. It doesn't mean rewriting your life story for every application. It means spending 20 to 30 minutes making sure the words in the posting that genuinely describe you also appear in your resume, in the places that matter. Here's the process I use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5wra6sijnpj40562ndf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5wra6sijnpj40562ndf.png" alt="A laptop comparing a resume against a job description, with a match score and keyword checklist" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why exact wording matters
&lt;/h2&gt;

&lt;p&gt;Applicant tracking systems store your resume as text. Recruiters then filter and search that text, often by skills, job titles and tools. Some systems are smarter about synonyms than others, but you can't know which one you're dealing with, so the safest assumption is simple: &lt;strong&gt;if the posting says it and you have it, say it the same way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Humans skim too. A hiring manager gives a resume a few seconds on the first pass. When the first third of the page mirrors the language of their own job description, it reads as a fit before they've thought about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pull the keywords out of the posting
&lt;/h2&gt;

&lt;p&gt;Copy the full job description into a plain text file, then sort what you find into four buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hard skills and tools:&lt;/strong&gt; languages, frameworks, platforms, certifications (Python, React, AWS, Kubernetes, Tableau, PMP).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job titles and role terms:&lt;/strong&gt; "Senior Backend Engineer," "Product Analyst," "Customer Success Manager."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain words:&lt;/strong&gt; "payments," "B2B SaaS," "healthcare compliance," "high-traffic e-commerce."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft skills tied to outcomes:&lt;/strong&gt; "stakeholder management," "mentoring," "cross-functional delivery."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pay attention to &lt;em&gt;where&lt;/em&gt; each term appears. Words under "Requirements," "Must have" or "Minimum qualifications" matter more than words under "Nice to have." Words that repeat across the responsibilities and the requirements are usually the core of the role.&lt;/p&gt;

&lt;p&gt;If you'd rather not do this by hand, I built a free &lt;a href="https://smartresumeanalyzer.com/job-description-keyword-finder?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=tailor-resume-keywords" rel="noopener noreferrer"&gt;job description keyword finder&lt;/a&gt; that runs in your browser. Paste the posting, and it groups possible keywords and flags terms tied to requirement cues. You can also paste your resume to see the text overlap. Treat its output as a starting list, not as the employer's secret scoring rules. Always check each term against the original wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Mark what you genuinely have
&lt;/h2&gt;

&lt;p&gt;Go through your list and mark each keyword:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Have it, already on my resume&lt;/strong&gt; (same wording)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have it, but worded differently&lt;/strong&gt; (you wrote "CI pipelines," they wrote "GitHub Actions")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partially have it&lt;/strong&gt; (used it on one project, not in production)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't have it&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second group is where most of the easy wins are. The last group is where people get into trouble. &lt;strong&gt;Never add a skill you don't have.&lt;/strong&gt; It might get you past a filter, but it will come apart in the first technical interview, and that's a worse outcome than not getting the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Put keywords where they carry weight
&lt;/h2&gt;

&lt;p&gt;Dumping 40 terms into a skills section is the most common mistake I see. It looks like a keyword list because it is one. Instead, spread the important terms across three places:&lt;/p&gt;

&lt;h3&gt;
  
  
  Your headline and summary
&lt;/h3&gt;

&lt;p&gt;The top of the page should use the job title from the posting (if it honestly matches your level) and two or three of its core requirements.&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Software engineer with experience building web applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Backend engineer with 4 years building Python and PostgreSQL services for payments products, focused on API reliability and performance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same person, same experience. The second version just speaks the posting's language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your experience bullets
&lt;/h3&gt;

&lt;p&gt;This is where keywords prove themselves. A skill mentioned inside a real accomplishment is far more convincing than a skill in a list.&lt;/p&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built reporting tools for the support team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built Python reporting scripts and SQL queries used daily by the support team, cutting weekly report prep from 5 hours to 1.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Only include numbers you can back up. If you don't have a metric, describe the scope instead (team size, number of users, frequency).&lt;/p&gt;

&lt;h3&gt;
  
  
  Your skills section
&lt;/h3&gt;

&lt;p&gt;Use this for exact-match coverage of the tools you really use. Group them so a human can scan it: &lt;em&gt;Languages, Frameworks, Cloud &amp;amp; DevOps, Data.&lt;/em&gt; Include both the acronym and the full term once where it's common, for example "Amazon Web Services (AWS)" or "Search Engine Optimization (SEO)."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Match the posting's spelling and format
&lt;/h2&gt;

&lt;p&gt;Small differences can matter to a text search:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spelling and casing:&lt;/strong&gt; "Node.js" vs "NodeJS," "PostgreSQL" vs "Postgres." Use the version in the posting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Singular vs plural and verb forms:&lt;/strong&gt; "data pipeline" vs "data pipelines." Not usually a dealbreaker, but mirroring is free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certifications:&lt;/strong&gt; write the full official name at least once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job titles:&lt;/strong&gt; if your official title was unusual ("Engineer II, Platform Squad"), you can add a clarifying title in brackets, for example "Engineer II (Backend Engineer)," as long as it's accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Keep it readable for humans
&lt;/h2&gt;

&lt;p&gt;A tailored resume should read better, not worse. A quick self-check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does each bullet still say what you did and why it mattered?&lt;/li&gt;
&lt;li&gt;Does any keyword appear so often it sounds robotic? Two or three natural mentions of a core skill are plenty.&lt;/li&gt;
&lt;li&gt;Could you talk comfortably for two minutes about every skill on the page?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a sentence only exists to hold a keyword, rewrite it or delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Check the match before you send
&lt;/h2&gt;

&lt;p&gt;Once you've edited, compare the resume against the posting one more time. Look for required terms you have but still haven't mentioned, and for formatting that might trip up parsing (tables, text in headers, multi-column layouts, skills inside images).&lt;/p&gt;

&lt;p&gt;This is exactly the loop I built Smart Resume Analyzer for. You upload your resume, paste the job description, and it shows an ATS-style score, missing keywords and suggestions for rewording. I use it as a final check, then I read the resume once more as a human before sending.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make tailoring fast: the base + variant method
&lt;/h2&gt;

&lt;p&gt;Tailoring every application from scratch is exhausting. What works better:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep one &lt;strong&gt;master resume&lt;/strong&gt; with every project, tool and metric you could ever use. It can be three pages; nobody sees it.&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;base version&lt;/strong&gt; for each type of role you apply to (for example "backend," "data engineering").&lt;/li&gt;
&lt;li&gt;For each application, copy the closest base and spend 20 minutes on Steps 1 to 6.&lt;/li&gt;
&lt;li&gt;Save each version with the company and role in the file name so you know what you sent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the first few, you'll find that most postings in your field share 70 to 80 percent of their keywords. Tailoring becomes a short edit rather than a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keywords pulled from the posting and sorted by importance&lt;/li&gt;
&lt;li&gt;Job title and two or three core skills in the headline or summary&lt;/li&gt;
&lt;li&gt;Key skills proven inside experience bullets&lt;/li&gt;
&lt;li&gt;Spelling matches the posting (Node.js, PostgreSQL, etc.)&lt;/li&gt;
&lt;li&gt;Acronym and full term used once for common terms&lt;/li&gt;
&lt;li&gt;No skills you can't defend in an interview&lt;/li&gt;
&lt;li&gt;Simple single-column layout, standard section headings&lt;/li&gt;
&lt;li&gt;Final match check against the posting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it on your next application
&lt;/h2&gt;

&lt;p&gt;Pick the job you most want right now and run through this once. Start with the &lt;a href="https://smartresumeanalyzer.com/job-description-keyword-finder?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=tailor-resume-keywords" rel="noopener noreferrer"&gt;keyword finder&lt;/a&gt;, make your edits, then run the result through the full checker on &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=tailor-resume-keywords" rel="noopener noreferrer"&gt;smartresumeanalyzer.com&lt;/a&gt;. The checker and keyword finder are free to use.&lt;/p&gt;

&lt;p&gt;If you want to find it again later, just bookmark &lt;a href="https://smartresumeanalyzer.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=how-to-tailor-your-resume-keywords-to-a-job-description-without-keyword-stuffing" rel="noopener noreferrer"&gt;Smart Resume Analyzer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's the hardest part of tailoring for you: finding the keywords, or fitting them in without sounding forced? Tell me in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  resume #jobsearch #ats #careeradvice #resumetips #jobhunting #hiring #careers #interviewtips #techjobs #career #productivity
&lt;/h1&gt;

</description>
      <category>career</category>
      <category>resume</category>
      <category>jobsearch</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Launching an Online Store in Pakistan: A Practical Guide to Platforms, Payments, COD, and Hosting</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:06:36 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/launching-an-online-store-in-pakistan-a-practical-guide-to-platforms-payments-cod-and-hosting-155e</link>
      <guid>https://dev.to/muhammadalimughal/launching-an-online-store-in-pakistan-a-practical-guide-to-platforms-payments-cod-and-hosting-155e</guid>
      <description>&lt;p&gt;Every month I talk to Pakistani business owners who want to sell online. A clothing brand in Karachi that's outgrown Instagram DMs. A wholesaler in Lahore tired of taking orders on WhatsApp. A home-based bakery that wants a proper checkout.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I run &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=online-store-pakistan" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;, a Karachi-based team that builds e-commerce stores, custom web apps, and the servers they run on. The questions I hear are almost always the same: &lt;em&gt;Which platform? Which payment gateway? What about cash on delivery? Where do I host it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This guide answers those questions plainly, including when you &lt;strong&gt;don't&lt;/strong&gt; need a custom build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Decide what kind of store you actually are
&lt;/h2&gt;

&lt;p&gt;Before picking technology, be honest about where your business is today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing the market&lt;/strong&gt; (under ~50 products, a few orders a day): you need something live this month, cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing retail brand&lt;/strong&gt; (hundreds of SKUs, sizes and colors, flash sales): you need solid variant and inventory handling, and a checkout that doesn't leak orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2B or wholesale&lt;/strong&gt; (buyer-specific pricing, minimum order quantities, order approvals): standard storefronts start fighting you here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your answer drives every decision below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose a platform: Shopify vs WooCommerce vs custom Laravel
&lt;/h2&gt;

&lt;p&gt;There's no universally "best" platform. Here's how I explain the trade-offs to clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shopify
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; getting started quickly with minimal technical work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosted for you, with polished themes and a big app ecosystem.&lt;/li&gt;
&lt;li&gt;Monthly subscription in USD, plus app fees that add up. Exchange-rate swings make that bill unpredictable.&lt;/li&gt;
&lt;li&gt;Shopify Payments isn't available for Pakistani merchants, so local payments depend on third-party apps or manual methods.&lt;/li&gt;
&lt;li&gt;You're limited to what the platform and apps allow, especially at checkout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  WooCommerce (WordPress)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; small to mid-size stores with a budget for a decent developer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free core software, huge plugin library, and plenty of local developers.&lt;/li&gt;
&lt;li&gt;Performance and security depend heavily on hosting and plugin discipline. Twenty plugins from twenty authors is a maintenance risk.&lt;/li&gt;
&lt;li&gt;Fine for a lot of stores, as long as someone keeps it updated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Custom Laravel store
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt; businesses whose workflows don't fit a template, or stores scaling past what plugins handle well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built around your catalog, checkout, and operations, with no plugin bloat.&lt;/li&gt;
&lt;li&gt;You own the code, with full control over pages, checkout steps, and integrations (couriers, ERP, WhatsApp, SMS).&lt;/li&gt;
&lt;li&gt;Higher upfront cost and longer timeline. At Loop Origin, a standard custom store starts from PKR 250,000 and usually takes 6 to 10 weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My honest rule of thumb:&lt;/strong&gt; if you're still testing whether people will buy, start with Shopify or a well-built WooCommerce site. Move to custom when COD returns, variant chaos, wholesale rules, or plugin conflicts start costing you real money. I wrote a deeper comparison here: &lt;a href="https://looporigin.com/blog/laravel-vs-wordpress-ecommerce-pakistan?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=online-store-pakistan" rel="noopener noreferrer"&gt;Laravel vs WordPress for e-commerce in Pakistan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Set up payments that Pakistani customers actually use
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foakafjllul68xjypa0r8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foakafjllul68xjypa0r8.png" alt="Order journey: checkout on a phone, branching into wallet/card payment or cash on delivery, then warehouse, courier, and doorstep, with a secured server underneath" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile wallets: JazzCash and Easypaisa
&lt;/h3&gt;

&lt;p&gt;Millions of Pakistanis pay with mobile wallets, and both JazzCash and Easypaisa offer merchant payment gateways for websites. Practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start the merchant onboarding early.&lt;/strong&gt; You'll typically need business documents and a bank account, and approval can take a while. Don't leave it for launch week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrate webhooks or payment callbacks properly.&lt;/strong&gt; A customer closing the browser after paying shouldn't leave the order stuck as "pending." Your server should confirm payment status directly with the gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test in sandbox first&lt;/strong&gt;, including failed and cancelled payments, not just the happy path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cards
&lt;/h3&gt;

&lt;p&gt;Many Pakistani banks and payment service providers offer card acceptance for merchants. If you sell internationally, Stripe may be an option depending on where your business is registered. Check eligibility before you design around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cash on delivery (COD)
&lt;/h3&gt;

&lt;p&gt;For most Pakistani retail stores, COD is still a huge share of orders. Ignore it and you lose customers. Handle it carelessly and you lose money on returns.&lt;/p&gt;

&lt;p&gt;Ways to keep COD healthy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm orders before dispatch.&lt;/strong&gt; An automated WhatsApp or SMS confirmation (or a quick call for high-value orders) filters out fake and impulse orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track return rates by city and customer.&lt;/strong&gt; Repeat refusers should be moved to prepaid-only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer a small discount for prepaid orders&lt;/strong&gt; to nudge customers toward wallets and cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show delivery charges and timelines clearly&lt;/strong&gt; on the product page, not as a surprise at checkout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's more on this in my guide to &lt;a href="https://looporigin.com/blog/cod-ecommerce-pakistan?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=online-store-pakistan" rel="noopener noreferrer"&gt;COD e-commerce in Pakistan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect couriers and inventory early
&lt;/h2&gt;

&lt;p&gt;Couriers like TCS, Leopards, PostEx, and Trax offer APIs or merchant portals for booking and tracking. Integrating early saves hours of copy-pasting addresses. At minimum, your store should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate shipping labels or booking requests from the order screen.&lt;/li&gt;
&lt;li&gt;Store tracking numbers and show them to customers.&lt;/li&gt;
&lt;li&gt;Update stock automatically when orders are placed, cancelled, or returned.&lt;/li&gt;
&lt;li&gt;Alert you when a size or color is running low.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inventory mistakes, like selling an item you don't have, hurt trust more than slow delivery does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Choose hosting that matches your traffic
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shopify:&lt;/strong&gt; hosting is included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WooCommerce:&lt;/strong&gt; avoid the cheapest shared hosting. A slow checkout loses orders. A small VPS or quality managed WordPress host is worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel:&lt;/strong&gt; a properly configured VPS or cloud server (Nginx, SSL, Redis for caching and queues, automated backups, uptime monitoring).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever you choose, insist on three things: &lt;strong&gt;SSL everywhere, daily off-site backups, and monitoring&lt;/strong&gt; that alerts someone when the site goes down. For the trade-offs, see &lt;a href="https://looporigin.com/blog/vps-vs-shared-hosting-pakistan?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=online-store-pakistan" rel="noopener noreferrer"&gt;VPS vs shared hosting in Pakistan&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Get the basics right before launch
&lt;/h2&gt;

&lt;p&gt;A pre-launch checklist I use with clients:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Mobile-first product pages that load fast on 4G&lt;/li&gt;
&lt;li&gt;[ ] Clear size charts, real product photos, and visible prices in PKR&lt;/li&gt;
&lt;li&gt;[ ] Delivery charges, timelines, and return policy published&lt;/li&gt;
&lt;li&gt;[ ] Wallet payments tested in sandbox &lt;em&gt;and&lt;/em&gt; with a small live transaction&lt;/li&gt;
&lt;li&gt;[ ] COD confirmation flow working&lt;/li&gt;
&lt;li&gt;[ ] Courier booking and tracking tested end to end&lt;/li&gt;
&lt;li&gt;[ ] Order confirmation emails and SMS/WhatsApp messages working&lt;/li&gt;
&lt;li&gt;[ ] Google Analytics or similar installed; Search Console verified&lt;/li&gt;
&lt;li&gt;[ ] Backups and uptime monitoring confirmed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a custom build makes sense
&lt;/h2&gt;

&lt;p&gt;You probably need a custom store if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're paying for a stack of apps or plugins just to make checkout work.&lt;/li&gt;
&lt;li&gt;You sell wholesale with buyer-specific pricing or approval workflows.&lt;/li&gt;
&lt;li&gt;You need deep integrations with an ERP, POS, or warehouse system.&lt;/li&gt;
&lt;li&gt;Your COD returns are high and you need confirmation and blacklisting logic built in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's exactly the kind of work we do at Loop Origin. You can see what's included on our &lt;a href="https://looporigin.com/services/ecommerce-website-development?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=online-store-pakistan" rel="noopener noreferrer"&gt;e-commerce website development&lt;/a&gt; page. Examples include a COD-first clothing storefront and a B2B wholesale platform.&lt;/p&gt;

&lt;p&gt;If you want to find us later, bookmark &lt;a href="https://looporigin.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launching-an-online-store-in-pakistan-a-practical-guide-to-platforms-payments-cod-and-hosting" rel="noopener noreferrer"&gt;Loop Origin&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The platform matters less than people think. What wins in Pakistan is a store that &lt;strong&gt;loads fast on mobile, takes the payments people actually use, confirms COD orders, and ships reliably.&lt;/strong&gt; Get those four right, on whatever platform fits your stage, and you're ahead of most competitors.&lt;/p&gt;

&lt;p&gt;What's the biggest hurdle you've hit selling online in Pakistan? Payments, COD returns, or couriers? Tell me in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  ecommerce #Pakistan #onlinestore #Laravel #webdevelopment #JazzCash #Easypaisa #cashondelivery #Shopify #WooCommerce #Karachi #smallbusiness #ecommercePakistan #startup #digitalPakistan
&lt;/h1&gt;

</description>
      <category>ecommerce</category>
      <category>laravel</category>
      <category>webdev</category>
      <category>pakistan</category>
    </item>
    <item>
      <title>How to Hide a Secret Message Inside an Image: A Practical Guide to Steganography</title>
      <dc:creator>Muhammad Ali Mughal</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:05:50 +0000</pubDate>
      <link>https://dev.to/muhammadalimughal/how-to-hide-a-secret-message-inside-an-image-a-practical-guide-to-steganography-29jp</link>
      <guid>https://dev.to/muhammadalimughal/how-to-hide-a-secret-message-inside-an-image-a-practical-guide-to-steganography-29jp</guid>
      <description>&lt;p&gt;Encryption hides &lt;em&gt;what&lt;/em&gt; a message says. Steganography hides the fact that there's a message at all.&lt;/p&gt;

&lt;p&gt;I'm Muhammad Ali Mughal, and I built &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=hide-message-in-image" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;, a browser-based tool for hiding text inside images. While building it, I learned that most people get two things wrong about steganography. Some think it's magic, and some think it's useless. It's neither. It's a simple technique with real strengths and real limits, and once you see how it works you'll know when to use it.&lt;/p&gt;

&lt;p&gt;This guide covers what image steganography is, how to hide a message step by step, where it's actually useful, and the mistakes that quietly destroy hidden data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What image steganography actually is
&lt;/h2&gt;

&lt;p&gt;Steganography means "covered writing." The digital version tucks data inside an ordinary-looking file, usually an image, so that anyone looking at it sees a normal photo.&lt;/p&gt;

&lt;p&gt;The most common method is &lt;strong&gt;LSB (least significant bit) embedding&lt;/strong&gt;. Here's the idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every pixel in a typical image has three color channels: red, green, and blue.&lt;/li&gt;
&lt;li&gt;Each channel is a number from 0 to 255, stored as 8 bits.&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;last&lt;/em&gt; bit of each number (the least significant bit) barely affects the color. Changing a red value from 200 to 201 is invisible to the human eye.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you take your message, turn it into bits, and write those bits into the last bit of each color channel. The picture looks the same. The data is there for anyone who knows to read it back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nedx0sjfacy15anub81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2nedx0sjfacy15anub81.png" alt="Three-step flow: a sealed message is encrypted, turned into bits written into the last bit of each pixel, and saved into an image that looks unchanged" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How much can you hide?
&lt;/h3&gt;

&lt;p&gt;With one bit per channel, you get 3 bits per pixel. That adds up quickly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image size&lt;/th&gt;
&lt;th&gt;Pixels&lt;/th&gt;
&lt;th&gt;Approx. capacity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;800 × 600&lt;/td&gt;
&lt;td&gt;480,000&lt;/td&gt;
&lt;td&gt;~175 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1920 × 1080&lt;/td&gt;
&lt;td&gt;~2.07 million&lt;/td&gt;
&lt;td&gt;~750 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4000 × 3000&lt;/td&gt;
&lt;td&gt;12 million&lt;/td&gt;
&lt;td&gt;~4.3 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real tools reserve a little space for a header (message length, format markers), and encryption adds some overhead, but for text you'll almost never run out of room. A full page of text is only a few kilobytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you should encrypt before you hide
&lt;/h2&gt;

&lt;p&gt;LSB embedding on its own is &lt;strong&gt;not security&lt;/strong&gt;. It's obscurity. Anyone who suspects an image contains data can run a standard extraction script and read raw LSB bits. If your message is plain text, it falls right out.&lt;/p&gt;

&lt;p&gt;That's why I designed PixelValut to encrypt the message with AES-256 (using the browser's Web Crypto API) &lt;em&gt;before&lt;/em&gt; embedding it. Now there are two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Steganography&lt;/strong&gt; hides the existence of the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption&lt;/strong&gt; protects the content if someone finds it anyway.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without the password, extracted bits look like random noise, which is exactly what LSB data in a natural photo should look like.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to hide a message in an image, step by step
&lt;/h2&gt;

&lt;p&gt;Whether you use PixelValut or another tool, the workflow is the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Pick the right cover image
&lt;/h3&gt;

&lt;p&gt;This matters more than people think.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a photo with lots of natural detail&lt;/strong&gt;: foliage, gravel, clouds, textured walls. Busy images hide changes better than flat ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid big areas of solid color&lt;/strong&gt; like a plain sky gradient or a logo on white. Statistical tools can spot LSB changes more easily in smooth regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use an original photo you took&lt;/strong&gt;, not a famous image from the internet. If someone has the original, comparing the two files reveals that something changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Write your message and choose a strong password
&lt;/h3&gt;

&lt;p&gt;Keep the message plain text. For the password, use a long passphrase (four or five random words) or a generated password. A random &lt;a href="https://pixelvalut.com/tools/password-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=hide-message-in-image" rel="noopener noreferrer"&gt;password generator&lt;/a&gt; helps here. The encryption is only as strong as the password protecting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Embed the message
&lt;/h3&gt;

&lt;p&gt;Upload the cover image, paste your message, enter the password, and run the encode step. The tool converts your message to bits, encrypts it, and writes it into the pixel data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Download as PNG, and keep it PNG
&lt;/h3&gt;

&lt;p&gt;This is the step that breaks most hidden messages. &lt;strong&gt;Save and share the output as PNG.&lt;/strong&gt; PNG is lossless, so every bit you wrote stays exactly where you put it.&lt;/p&gt;

&lt;p&gt;JPG is lossy. When an image is saved or re-saved as JPG, the compression changes pixel values slightly. That's harmless to the eye and fatal to LSB data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Test the round trip before you send it
&lt;/h3&gt;

&lt;p&gt;Before sharing the image, decode it yourself: upload the PNG, enter the password, and confirm the message comes back intact. It takes 30 seconds and saves a lot of confusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Share the password through a different channel
&lt;/h3&gt;

&lt;p&gt;If you send the image by email, don't put the password in the same email. Share it in person, over a call, or through a separate messaging app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where steganography is actually useful
&lt;/h2&gt;

&lt;p&gt;Steganography isn't just for spy movies. Practical uses I've seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watermarking and proof of ownership.&lt;/strong&gt; Photographers and designers embed an invisible signature or ID in their work. If the image gets reposted, you can prove where it came from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper detection.&lt;/strong&gt; If the hidden data no longer decodes correctly, you know the image was edited or re-compressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private notes in plain sight.&lt;/strong&gt; Some people store an encrypted note (a recovery phrase hint, a private journal entry) inside an ordinary photo in their gallery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teaching security concepts.&lt;/strong&gt; It's a great hands-on way to show students how binary, color channels, and encryption work together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTF challenges and puzzles.&lt;/strong&gt; Capture-the-flag competitions love hidden messages in images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations you need to know
&lt;/h2&gt;

&lt;p&gt;Being honest about limits is part of using any security tool well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Social media will destroy your message
&lt;/h3&gt;

&lt;p&gt;Facebook, Instagram, WhatsApp (in normal photo mode), X, and most messaging apps &lt;strong&gt;re-compress uploaded images&lt;/strong&gt;. They resize them, convert them to JPG, and strip metadata. Your hidden data won't survive.&lt;/p&gt;

&lt;p&gt;To keep the data intact, send the image as a &lt;strong&gt;file or document&lt;/strong&gt; rather than as a photo, or use cloud storage links and email attachments that keep the original file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing breaks it too
&lt;/h3&gt;

&lt;p&gt;Cropping, resizing, filters, rotation, and screenshots all rewrite pixel values. Treat the output image as read-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detection is possible
&lt;/h3&gt;

&lt;p&gt;Steganalysis tools can detect LSB embedding statistically, especially with large payloads in smooth images. Steganography reduces attention; it doesn't make detection impossible. Keep payloads small relative to image size, and rely on encryption for actual confidentiality.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's not a replacement for proper secure messaging
&lt;/h3&gt;

&lt;p&gt;If you just need private communication, an end-to-end encrypted messenger is the right tool. Steganography makes sense when you specifically need the message to be &lt;em&gt;invisible&lt;/em&gt;, or when you're watermarking, verifying, or learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick security checklist
&lt;/h2&gt;

&lt;p&gt;Before you hide anything important, run through this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Original, detailed photo (not a downloaded image, not flat colors)&lt;/li&gt;
&lt;li&gt;[ ] Message encrypted with a strong, unique password&lt;/li&gt;
&lt;li&gt;[ ] Output saved as &lt;strong&gt;PNG&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Round-trip decode tested&lt;/li&gt;
&lt;li&gt;[ ] Sent as a file, not through a photo uploader that re-compresses&lt;/li&gt;
&lt;li&gt;[ ] Password shared over a separate channel&lt;/li&gt;
&lt;li&gt;[ ] Original cover image deleted if you don't want comparisons possible&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;If you want to experiment, &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=hide-message-in-image" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt; lets you hide and reveal AES-encrypted text in images, with processing done in your browser. I also wrote a shorter walkthrough on the site: &lt;a href="https://pixelvalut.com/blog/steganography/how-to-hide-messages-in-images?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=hide-message-in-image" rel="noopener noreferrer"&gt;how to hide messages in images&lt;/a&gt;. Alongside steganography, the site has free developer utilities like DNS and MX lookups, a JWT decoder, and an SSL checker that I use in my own work.&lt;/p&gt;

&lt;p&gt;If you're looking for it later, just bookmark &lt;a href="https://pixelvalut.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=how-to-hide-a-secret-message-inside-an-image-a-practical-guide-to-steganography" rel="noopener noreferrer"&gt;PixelValut&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Have you used steganography for watermarking, CTFs, or something unusual? I'd like to hear about it in the comments.&lt;/p&gt;

&lt;h1&gt;
  
  
  steganography #cybersecurity #infosec #privacy #encryption #cryptography #datasecurity #AES256 #imagesecurity #hiddenmessage #digitalwatermark #CTF #websecurity #devtools #ethicalhacking
&lt;/h1&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>cryptography</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
