<?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: charlie-morrison</title>
    <description>The latest articles on DEV Community by charlie-morrison (@charliemorrison).</description>
    <link>https://dev.to/charliemorrison</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%2F3896832%2Fab355440-b976-4d9a-b9fe-762faf3e7836.png</url>
      <title>DEV Community: charlie-morrison</title>
      <link>https://dev.to/charliemorrison</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charliemorrison"/>
    <language>en</language>
    <item>
      <title>Telegram's 4096-character limit isn't characters. I measured it</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:22:18 +0000</pubDate>
      <link>https://dev.to/charliemorrison/telegrams-4096-character-limit-isnt-characters-i-measured-it-331</link>
      <guid>https://dev.to/charliemorrison/telegrams-4096-character-limit-isnt-characters-i-measured-it-331</guid>
      <description>&lt;p&gt;If you have ever asked how long a Telegram message can be, you have met the same answer twice: 4096 characters, and by the way Telegram counts UTF-16, so an emoji costs two. It is repeated in library issues, in Stack Overflow answers, and in the defensive splitters people paste into their bots, usually as a chunk size of 2000 or 4000 chosen with a shrug for safety.&lt;/p&gt;

&lt;p&gt;I write a lot of bot code that emits long, emoji-heavy status blocks, and that folklore was costing me splits I did not think I needed. So I stopped guessing and asked the API directly: send messages one unit either side of the boundary, in four different alphabets, and see which measurement predicts what the server does.&lt;/p&gt;

&lt;p&gt;The answer is that the number 4096 is real and exact, and that the unit almost everybody names is the wrong one. Worse, the UTF-16 rule &lt;em&gt;is&lt;/em&gt; true, of a different field, in the same reply. A single &lt;code&gt;Message&lt;/code&gt; object mixes two units, which is precisely why the folklore has survived so long: everyone is half right.&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%2F4v89s51695arzd1jxl05.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%2F4v89s51695arzd1jxl05.png" alt="Terminal output of the probe: ascii, cyrillic, emoji and combining-character messages at 4096 and 4097 code points, showing that code points predict every accept or reject while UTF-16 length does not, and that entity offsets use offset 2 after a single emoji" width="800" height="422"&gt;&lt;/a&gt; Unedited output from the probe, 10 August 2026. Every number in this post comes from this run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;The probe builds strings out of four deliberately different characters and sends each through &lt;a href="https://core.telegram.org/bots/api#sendmessage" rel="noopener noreferrer"&gt;&lt;code&gt;sendMessage&lt;/code&gt;&lt;/a&gt; against a real bot token (a throwaway bot, never one serving users), recording the HTTP status and the exact &lt;code&gt;description&lt;/code&gt; Telegram returns. Every message it manages to send is deleted with &lt;code&gt;deleteMessage&lt;/code&gt; in the same breath, so the run leaves nothing behind in the chat.&lt;/p&gt;

&lt;p&gt;The four characters are chosen so that the candidate units disagree with each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; -- 1 code point, 1 UTF-16 unit, 1 byte. All three units agree, so this only finds the number.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;я&lt;/code&gt; -- 1 code point, 1 UTF-16 unit, &lt;strong&gt;2 bytes&lt;/strong&gt;. Separates bytes from the rest.&lt;/li&gt;
&lt;li&gt;😀 (U+1F600) -- 1 code point, &lt;strong&gt;2 UTF-16 units&lt;/strong&gt; , 4 bytes. Separates code points from UTF-16.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;e&lt;/code&gt; + combining acute (U+0301) -- &lt;strong&gt;2 code points&lt;/strong&gt; , 2 UTF-16 units, 3 bytes, and &lt;strong&gt;one thing you can see&lt;/strong&gt;. Separates all of them from "visible characters".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the only discipline that matters: test the unit immediately either side of every candidate boundary. A limit you have bracketed to the nearest hundred is a limit you have not found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number is 4096. The unit is code points.
&lt;/h2&gt;

&lt;p&gt;ASCII gives the number straight away. 4,096 characters go through; 4,097 come back as a clean &lt;code&gt;400 Bad Request: message is too long&lt;/code&gt;. No truncation, no silent trim -- a real error you can catch.&lt;/p&gt;

&lt;p&gt;Cyrillic kills the bytes hypothesis. 4,096 Cyrillic characters are &lt;strong&gt;8,192 bytes&lt;/strong&gt; of UTF-8, twice the ASCII payload, and Telegram accepts them; 4,097 fails. So whatever is being counted, it is not the size of what goes on the wire.&lt;/p&gt;

&lt;p&gt;The emoji case is the one that surprised me. If the cap counted UTF-16 code units, 2,049 emoji -- 4,098 units -- would be rejected. It was accepted. So I pushed until it broke, and the boundary sits exactly where a code-point counter would put it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;emoji x4096   code pts 4096   UTF-16 8192   bytes 16384   -&amp;gt; OK
emoji x4097   code pts 4097   UTF-16 8194   bytes 16388   -&amp;gt; 400 message is too long
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Read that again, because it is the whole post. &lt;strong&gt;A single Telegram message can carry 4,096 emoji, 16 KB of UTF-8 and 8,192 UTF-16 code units, and the server takes it.&lt;/strong&gt; I echoed the accepted message back out of the API response and compared it to what I sent: identical, character for character. Nothing was truncated on the way through.&lt;/p&gt;

&lt;p&gt;The mixed case pins it from the other side. &lt;code&gt;a&lt;/code&gt; × 4,095 followed by one emoji is 4,096 code points and 4,097 UTF-16 units: accepted. Add one more &lt;code&gt;a&lt;/code&gt; and it is 4,097 code points: rejected. Across every probe I ran, "code points ≤ 4096" predicted the outcome every single time. "UTF-16 units ≤ 4096" did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  But the UTF-16 rule is real -- for offsets
&lt;/h2&gt;

&lt;p&gt;Here is why the folklore refuses to die. In the same JSON reply that just accepted 8,192 UTF-16 units, the formatting entities are indexed in UTF-16.&lt;/p&gt;

&lt;p&gt;Send one emoji followed by bold text and read the entity Telegram hands back:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;😀*bold*     -&amp;gt;  {"offset": 2, "length": 4, "type": "bold"}
😀😀😀*bold*  -&amp;gt;  {"offset": 6, "length": 4, "type": "bold"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;One emoji, and the bold run starts at offset &lt;strong&gt;2&lt;/strong&gt;. Three emoji, offset &lt;strong&gt;6&lt;/strong&gt;. If offsets were code points those numbers would be 1 and 3. Telegram documents this (&lt;a href="https://core.telegram.org/api/entities" rel="noopener noreferrer"&gt;entity offsets are specified in UTF-16 code units&lt;/a&gt;) and it is correct, and it has been quietly transplanted onto the length cap by a thousand summarised answers.&lt;/p&gt;

&lt;p&gt;So both halves of the folklore are true of something. They are just true of different fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Python equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4096 text cap / 1024 caption cap&lt;/td&gt;
&lt;td&gt;Unicode code points&lt;/td&gt;
&lt;td&gt;&lt;code&gt;len(text)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;entities[].offset&lt;/code&gt; and &lt;code&gt;.length&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;UTF-16 code units&lt;/td&gt;
&lt;td&gt;&lt;code&gt;len(text.encode('utf-16-le')) // 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have ever sliced a message at an entity offset with plain Python indexing and watched the bold run land one character to the left for every emoji before it, that table is the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visible characters are not the unit either
&lt;/h2&gt;

&lt;p&gt;The combining-mark probe closes the last escape route. &lt;code&gt;e&lt;/code&gt; + U+0301 renders as a single é, but it is two code points. Send 2,048 of them, which is 2,048 things a human can see and 4,096 code points, and it is accepted. Send 2,049 and it fails at 4,098.&lt;/p&gt;

&lt;p&gt;So a user can paste 2,049 visible characters into your bot and be told their message is too long, and they will be right and the error will also be right. If you show a live character counter in a Mini App, this is the case that makes it disagree with the server. Any counter built on grapheme clusters, or on &lt;code&gt;String.length&lt;/code&gt; in JavaScript (which is UTF-16), will be wrong in one direction or the other for exactly these inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Captions: same rule, different number, different error string
&lt;/h2&gt;

&lt;p&gt;Captions cap at 1,024 and behave identically. 1,024 emoji, or 2,048 UTF-16 units, are accepted; 1,025 are rejected. Worth noting for log-grepping: the error text is not the same one &lt;code&gt;sendMessage&lt;/code&gt; returns.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sendMessage  -&amp;gt; 400 Bad Request: message is too long
sendPhoto    -&amp;gt; 400 Bad Request: message caption is too long
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A pleasant contrast with the file limits.&lt;/strong&gt; When I measured the file size boundaries, an over-limit upload got no error at all -- Telegram dropped the TLS connection ~30 seconds in and left the caller holding a transport exception. Text is the well-behaved case: you get a real 400, immediately, with a description worth logging. &lt;/p&gt;

&lt;h2&gt;
  
  
  What this means in code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In Python, the naive guard is the correct guard.&lt;/strong&gt; &lt;code&gt;len(text) &amp;lt;= 4096&lt;/code&gt; matched every outcome I measured. This is the rare case where the obvious thing is right and the clever thing, re-encoding to UTF-16 to "be safe", is what makes you wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In JavaScript, the naive guard is the wrong one.&lt;/strong&gt; &lt;code&gt;str.length&lt;/code&gt; is &lt;a href="https://www.unicode.org/faq/utf_bom.html" rel="noopener noreferrer"&gt;UTF-16 code units&lt;/a&gt;, so an emoji reads as 2 and your Mini App will refuse a message the server would have accepted. Count code points instead: &lt;code&gt;[...str].length&lt;/code&gt;, which iterates by code point and gives 1 per emoji.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop halving your chunk size for emoji.&lt;/strong&gt; If you split long output at 2,000 "to be safe with unicode", you are sending twice the messages you need to, at twice the flood-limit risk, for a hazard that does not exist. Split at 4,096 code points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never index a string by an entity offset directly.&lt;/strong&gt; Convert first: encode to &lt;code&gt;utf-16-le&lt;/code&gt;, slice by &lt;code&gt;offset * 2&lt;/code&gt; and &lt;code&gt;length * 2&lt;/code&gt;, decode back. Anything else is correct only until someone types an emoji.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building on Telegram for something lighter?
&lt;/h3&gt;

&lt;p&gt;My free planner lays out a Telegram game night -- rounds, timings, poll structure -- in about a minute, no signup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.dev/telegram-game-night-planner/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=telegram-4096-character-limit" rel="noopener noreferrer"&gt;Build a game night -&amp;gt;&lt;/a&gt; Or read how to run one end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;p&gt;Three honest gaps. I did not test whether the same code-point rule holds on the MTProto client APIs -- TDLib and the user-account layer are a different code path where the UTF-16 convention is far more visible, so I would not assume my result transfers. I did not test &lt;code&gt;editMessageText&lt;/code&gt;, only &lt;code&gt;sendMessage&lt;/code&gt; and &lt;code&gt;sendPhoto&lt;/code&gt;; I would expect the same cap but I have not seen it. And I did not probe the entity-count limit, which is a separate ceiling that bites long formatted messages before the length cap does.&lt;/p&gt;

&lt;p&gt;Everything here comes from one bot on one network path. I am comfortable with that for this particular claim, because the result is not a statistical trend -- it is a boundary that lands on an exact power of two from four directions at once, with a one-unit failure on the far side of each. Encoding a message differently changed its byte length fourfold and moved the boundary not at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cap is 4,096 Unicode code points&lt;/strong&gt; , exactly. 4,097 gives &lt;code&gt;400 message is too long&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not bytes:&lt;/strong&gt; 4,096 Cyrillic characters are 8,192 bytes and go through fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not UTF-16:&lt;/strong&gt; 4,096 emoji are 8,192 UTF-16 units and 16 KB, and go through fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not visible characters:&lt;/strong&gt; 2,049 combining-accent é's are 4,098 code points and are rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity offsets &lt;em&gt;are&lt;/em&gt; UTF-16.&lt;/strong&gt; Two units in one &lt;code&gt;Message&lt;/code&gt;. Convert before slicing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captions: same rule at 1,024&lt;/strong&gt; , with a distinct error string.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Questions I had before I measured
&lt;/h2&gt;

&lt;p&gt;What unit is Telegram's 4096 message limit counted in?&lt;br&gt;
    Unicode code points. Measured against a live bot, 4,096 code points is accepted and 4,097 is rejected with 400 Bad Request: message is too long, regardless of how those code points encode. A message of 4,096 emoji is 8,192 UTF-16 code units and 16,384 bytes of UTF-8, and it sends without complaint.&lt;br&gt;
Does an emoji count as two characters in a Telegram message?&lt;br&gt;
    Not against the length cap. A non-BMP emoji is one code point, and the cap counts code points, so it costs one. It does count as two in entity offsets, which are measured in UTF-16 code units. That is why the advice to budget two units per emoji is half right: it applies to formatting offsets, not to the 4096 limit.&lt;br&gt;
Is len(text) a correct check for the Telegram message limit in Python?&lt;br&gt;
    Yes. Python's len returns the number of Unicode code points, which is exactly the unit the cap uses, so len(text) &amp;lt;= 4096 matched every measured outcome. Guards written against UTF-16 length or UTF-8 byte length both reject messages that Telegram accepts.&lt;br&gt;
Does the same rule apply to the 1024 caption limit?&lt;br&gt;
    Yes. Captions cap at 1,024 code points and behave identically: 1,024 emoji is accepted at 2,048 UTF-16 units, and 1,025 is rejected with 400 Bad Request: message caption is too long. The error text differs from the sendMessage one, which is useful when you are reading logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telegram in Production -- the parts that bite you
&lt;/h3&gt;

&lt;p&gt;The guards from this post, finished: a length check in the unit the server actually uses, an entity-offset slicer that survives emoji, and a splitter that stops halving your chunks for no reason. Plus the file-size guard that fails loudly instead of retrying forever, an initData validator with &lt;code&gt;signature&lt;/code&gt; excluded and &lt;code&gt;auth_date&lt;/code&gt; enforced, poll payloads Telegram will not silently rewrite, and a systemd unit linter for the two failure modes that cost me weeks. 55 tests you can run from the zip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/buy/710851ec-08d5-447b-b022-1053d3469d15?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=telegram-4096-character-limit" rel="noopener noreferrer"&gt;Get the pack -- $19&lt;/a&gt; What is in the pack, module by module. Every claim in it was measured first and published here.&lt;/p&gt;

&lt;h2&gt;
  
  
  More from the Telegram build log
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Telegram bot file size limits: 20 MiB down, 50 MiB up, both exact to the byte -- and the upload cap counts your HTTP headers.&lt;/li&gt;
&lt;li&gt;I forged Telegram initData: which payloads pass validation, and the field that broke every old validator.&lt;/li&gt;
&lt;li&gt;I tested Telegram's poll limits: twelve options, and one timer behaviour that closes your round early.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/telegram-4096-character-limit/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>python</category>
      <category>webdev</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>Telegram Poll Limits: What I Measured Against the Live Bot API</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Tue, 04 Aug 2026 19:57:35 +0000</pubDate>
      <link>https://dev.to/charliemorrison/telegram-poll-limits-what-i-measured-against-the-live-bot-api-3opk</link>
      <guid>https://dev.to/charliemorrison/telegram-poll-limits-what-i-measured-against-the-live-bot-api-3opk</guid>
      <description>&lt;p&gt;A game night in a group chat lives or dies on polls. They are the only mechanic Telegram gives you where twelve people can answer at once without the first reply telling everyone else what to think. Every round I have ever run -- who is lying, which answer is right, who gets the dare -- ends in a poll.&lt;/p&gt;

&lt;p&gt;So it is worth knowing exactly where a poll stops working. Not the theory: the point where the API says no, and the more annoying point where it says yes and quietly does something else. I spent an evening sending deliberately-broken polls at Telegram until it complained, then read every poll back a second way to see what a person in the chat would actually get.&lt;/p&gt;

&lt;p&gt;Some of it matches &lt;a href="https://core.telegram.org/bots/api#sendpoll" rel="noopener noreferrer"&gt;the official &lt;code&gt;sendPoll&lt;/code&gt; reference&lt;/a&gt;. Three things do not, and one of those three has silently ruined a round for me before I understood it.&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%2Fgsvyq31ohx7kbm1hq82v.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%2Fgsvyq31ohx7kbm1hq82v.png" alt="Terminal output from three scripts probing the Telegram sendPoll API: length and option-count limits, timer clamping behaviour, and poll text read back over MTProto showing empty entity lists" width="800" height="651"&gt;&lt;/a&gt; Unedited output from the three probe scripts, 3 August 2026. Every number in this post comes from these runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;Three passes, because one was not enough to catch my own mistakes.&lt;/p&gt;

&lt;p&gt;The first pass sends polls at the boundary of every documented limit -- a 300-character question and a 301-character one, twelve options and thirteen, and so on -- and records the exact accept or reject. The second pass re-sends the interesting cases and reads what Telegram &lt;em&gt;echoes back&lt;/em&gt; in the response, because "accepted" and "sent as I asked" turn out to be different things. The third pass reads the same polls back over the client protocol with a normal user account, which is the closest I can get to seeing what a member of the group sees without asking anyone to look over my shoulder.&lt;/p&gt;

&lt;p&gt;That third pass exists because of a habit I had to learn the hard way: an exit code of zero tells you the request left the building, not that the thing arrived intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard walls
&lt;/h2&gt;

&lt;p&gt;These are the limits that produce a clean, immediate error. You will never hit them by accident with a short question, and you will hit all of them the moment you paste in something you wrote in a document.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;What happens past it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Question&lt;/td&gt;
&lt;td&gt;300 characters&lt;/td&gt;
&lt;td&gt;poll question length must not exceed 300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Options per poll&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;poll can't have more than 12 options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text per option&lt;/td&gt;
&lt;td&gt;100 characters&lt;/td&gt;
&lt;td&gt;poll options length must not exceed 100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quiz explanation&lt;/td&gt;
&lt;td&gt;200 characters&lt;/td&gt;
&lt;td&gt;message is too long&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty option&lt;/td&gt;
&lt;td&gt;not allowed&lt;/td&gt;
&lt;td&gt;text must be non-empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two identical options&lt;/td&gt;
&lt;td&gt;allowed&lt;/td&gt;
&lt;td&gt;accepted without complaint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twelve is the number that shapes a game night more than any other. A round where everybody votes for a person -- who is most likely to lose their phone, who is bluffing -- caps out at a group of twelve, because each player needs their own option. Past that you are splitting the room into two polls and reconciling the counts by hand, which is exactly as fun as it sounds. If your group runs bigger than twelve, design the round as a vote on &lt;em&gt;answers&lt;/em&gt; , not on &lt;em&gt;people&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;One hundred characters per option sounds generous until you write a Would You Rather. "Always have to say everything on your mind out loud, even in meetings" is 70. The version with the funny qualifier on the end is 118, and it is refused. The workaround is not to shorten the joke -- it is to put the full text in the message above the poll and keep the options to the two short labels people are choosing between.&lt;/p&gt;

&lt;h2&gt;
  
  
  The character cap counts characters, not bytes
&lt;/h2&gt;

&lt;p&gt;This one I expected to go the other way. An option of 100 dice emoji is 400 bytes on the wire, and it is accepted. One hundred and one of them is refused with the same error as 101 letters.&lt;/p&gt;

&lt;p&gt;So the cap is counted in characters as a person would count them, not in the storage a string takes up. That matters for anyone writing prompts with emoji in them, and doubly for anyone writing them in a language whose characters are multi-byte by default -- a Ukrainian or Greek option gets the same 100 characters as an English one, not half as many. &lt;a href="https://www.unicode.org/faq/utf_bom.html" rel="noopener noreferrer"&gt;The Unicode consortium's own FAQ&lt;/a&gt; is the reference for why those two counts differ so much in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timers get rounded, not refused
&lt;/h2&gt;

&lt;p&gt;Here is the behaviour that costs you a round. A poll can carry a countdown -- &lt;code&gt;open_period&lt;/code&gt; -- and the allowed range is 5 seconds to 2,628,000 seconds, which is 30 days.&lt;/p&gt;

&lt;p&gt;Send something outside that range and Telegram does not reject it. It &lt;strong&gt;silently rounds it into range and tells you nothing&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for a 4-second timer, get 5 seconds.&lt;/li&gt;
&lt;li&gt;Ask for 2,628,001 seconds, get 2,628,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response comes back &lt;code&gt;ok: true&lt;/code&gt; with the corrected number sitting in it, which is easy to miss if you are only checking that the call succeeded. And the corrected timer is real: I sent a poll asking for 4 seconds, waited nine, and tried to close it manually. Telegram refused -- &lt;em&gt;poll can't be stopped&lt;/em&gt; -- because it had already closed itself on schedule.&lt;/p&gt;

&lt;p&gt;The same forgiving-to-a-fault behaviour shows up in a second place. The documentation says &lt;code&gt;open_period&lt;/code&gt; and &lt;code&gt;close_date&lt;/code&gt; cannot be used together. Send both anyway and the request is accepted; the &lt;code&gt;open_period&lt;/code&gt; wins and the &lt;code&gt;close_date&lt;/code&gt; you specified is overwritten. I asked for a 60-second timer and a close time ten minutes out, and got a poll that closed in 60 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this bites in practice.&lt;/strong&gt; Timed rounds are where hosts reach for these values, usually by computing them from something -- "close this when the round ends", "give them a third of the remaining time". Arithmetic that lands on 3 or 4 seconds silently becomes 5, and arithmetic that produces both a period and a date silently drops one of them. Nothing errors, nothing logs, and the round just runs on a timer you did not choose. Print the value Telegram echoes back, not the one you sent. &lt;/p&gt;

&lt;h2&gt;
  
  
  There is no bold, no italics and no spoiler inside a poll
&lt;/h2&gt;

&lt;p&gt;Poll options carry no formatting at all. Not "limited formatting" -- none. I sent three options containing a spoiler tag, a bold marker and an HTML bold tag, then read the poll back over the client protocol. All three came back as literal text, with an empty entity list on every one of them.&lt;/p&gt;

&lt;p&gt;The question field is only slightly better: it accepts a parse mode, but &lt;a href="https://core.telegram.org/bots/api#sendpoll" rel="noopener noreferrer"&gt;the reference&lt;/a&gt; restricts it to custom emoji entities. I sent a question as MarkdownV2 bold; the asterisks were consumed and the text arrived plain, with no bold and no entities. Not an error -- just quietly unformatted.&lt;/p&gt;

&lt;p&gt;For a game night this rules out the trick everyone reaches for first: hiding the answer behind a spoiler tag inside the poll. &lt;code&gt;||like this||&lt;/code&gt; in an option is displayed exactly as those characters, answer and all. What works instead is a two-part round -- the hidden text goes in a normal message, where spoiler formatting is fully supported, and the poll below it carries only the plain-text choices. It is one extra message and it is the difference between a reveal and a leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quiz mode: one correct answer, whatever the field name suggests
&lt;/h2&gt;

&lt;p&gt;Quiz polls -- the mode &lt;a href="https://telegram.org/blog/polls-2-0-vmq" rel="noopener noreferrer"&gt;Telegram introduced with Polls 2.0&lt;/a&gt; -- mark one option right and can show an explanation when someone picks wrong. The parameter is now named in the plural, &lt;code&gt;correct_option_ids&lt;/code&gt;, which reads like an invitation to mark two answers correct.&lt;/p&gt;

&lt;p&gt;It is not. Passing two ids is refused with &lt;code&gt;QUIZ_CORRECT_ANSWERS_TOO_MUCH&lt;/code&gt;. The older singular parameter still works and comes back echoed in both the singular and plural fields, so nothing you already wrote is broken -- but a quiz round with two acceptable answers has to be built as two separate questions.&lt;/p&gt;

&lt;p&gt;Two smaller findings from the same pass. A quiz without a correct answer is refused outright (&lt;em&gt;correct quiz option list must be non-empty&lt;/em&gt;), so you cannot use quiz mode purely for its nicer layout. And the explanation is documented as allowing at most two line breaks -- I sent three and it was accepted, which is the one place the API turned out to be more permissive than its own description rather than less.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four switches worth knowing before your next round
&lt;/h2&gt;

&lt;p&gt;While checking the limits I went through the full parameter list, and a few of the newer ones solve group-chat problems I had been solving by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hide_results_until_closes&lt;/code&gt;&lt;/strong&gt; -- nobody sees the tally until the poll closes. This is the native fix for the single biggest problem with voting in a group: the first three votes anchor everyone who comes later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shuffle_options&lt;/code&gt;&lt;/strong&gt; -- each person sees the options in a different order, which kills "the answer is always the long one" pattern-matching in a quiz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allows_revoting&lt;/code&gt;&lt;/strong&gt; -- on by default for regular polls, off by default for quizzes. If your round is scored, turn it off explicitly and stop arguing about who changed their vote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allow_adding_options&lt;/code&gt;&lt;/strong&gt; -- lets players add their own answers, which is a genuinely good "make up a lie" round. It refuses to work on an anonymous poll: pair it with public voting or you get &lt;code&gt;ANONYMOUS_OPEN_INVALID&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An honest caveat on the first two: Telegram accepts both flags, but neither appears in the poll object it sends back, and neither showed up when I read the poll over the client protocol either. So I can confirm they are accepted -- I cannot confirm from the response alone that they took effect. Set them, then look at the poll in the chat before you build a round around them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Want the run sheet instead of the API?
&lt;/h3&gt;

&lt;p&gt;The free &lt;strong&gt;Telegram Game Night Planner&lt;/strong&gt; builds a timed game night as paste-ready blocks -- rounds, host lines, and poll questions with the options already separated so you can send them straight into a group chat. No signup, runs in your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.dev/telegram-game-night-planner/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=telegram-poll-limits-game-night" rel="noopener noreferrer"&gt;Build a game night -&amp;gt;&lt;/a&gt; Everything above is baked into the blocks it gives you -- short options, plain text, no spoiler tags where they will not render.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually changed after this
&lt;/h2&gt;

&lt;p&gt;Four rules, all of them the direct consequence of a probe above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Options stay short and plain.&lt;/strong&gt; The long version of the prompt goes in the message; the poll gets the two or three word labels. This sidesteps the 100-character wall entirely and reads better on a phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing hidden ever goes in an option.&lt;/strong&gt; Spoilers live in a normal message above the poll, because inside one they are just punctuation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twelve is the room size for person-voting rounds.&lt;/strong&gt; Bigger group, different round design -- vote on the answer, not the player.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read back the timer.&lt;/strong&gt; If a round is timed, use the number Telegram returns, not the one that was sent. It is the only way to notice a clamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic. It is the difference between a round that lands and a round where somebody says "wait, I can see the answer" -- which, in a group chat, is the whole game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running a whole night, not a single round?
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Telegram Party Pack&lt;/strong&gt; is the hosted version of everything above: 255 prompts across six games, a host guide for a 60-minute night in a group chat, and the poll, spoiler and threading mechanics laid out per round so nothing leaks and nothing anchors. Copy-paste blocks, PDF included.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/buy/c7bd4341-6eb3-4acc-b8e1-7946e1413b98?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=telegram-poll-limits-game-night" rel="noopener noreferrer"&gt;Get the pack -- $9.99&lt;/a&gt; The planner above stays free. See what is in the pack before you buy.&lt;/p&gt;

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

&lt;p&gt;How many options can a Telegram poll have?&lt;/p&gt;

&lt;p&gt;Twelve. The thirteenth is refused with "poll can't have more than 12 options". A single-option poll is also accepted, which is occasionally useful as a one-button prompt. For any round where each player needs their own option, twelve is your real group-size cap.&lt;/p&gt;

&lt;p&gt;How long can a poll option be?&lt;/p&gt;

&lt;p&gt;100 characters per option, 300 for the question, 200 for a quiz explanation. The count is per character, not per byte -- 100 emoji weigh 400 bytes and still pass, while 101 of anything fails.&lt;/p&gt;

&lt;p&gt;Can I use bold or a spoiler in a poll?&lt;/p&gt;

&lt;p&gt;No. Options carry no formatting entities at all, so markup appears verbatim. The question accepts a parse mode but only for custom emoji -- bold sent as MarkdownV2 arrives stripped. Put hidden text in a regular message and use the poll only for the vote.&lt;/p&gt;

&lt;p&gt;How long can a poll stay open?&lt;/p&gt;

&lt;p&gt;From 5 seconds to 30 days. Anything outside that is rounded into range without an error, so a 4-second timer quietly becomes 5. Sending an &lt;code&gt;open_period&lt;/code&gt; and a &lt;code&gt;close_date&lt;/code&gt; together is also accepted despite the documentation, and the &lt;code&gt;open_period&lt;/code&gt; is the one that wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The best Telegram party games in 2026 -- the rounds these polls are actually for.&lt;/li&gt;
&lt;li&gt;How to run a Telegram game night -- the host side: pacing, pinned rules, and keeping a round readable when replies land out of order.&lt;/li&gt;
&lt;li&gt;13 Telegram bots on a $4.17 VPS -- if you would rather have a bot send these polls for you, here is what that costs to host.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/telegram-poll-limits-game-night/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>telegram</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>13 Telegram Bots on One 1GB VPS: The Real RAM Numbers</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 03 Aug 2026 20:25:21 +0000</pubDate>
      <link>https://dev.to/charliemorrison/13-telegram-bots-on-one-1gb-vps-the-real-ram-numbers-5ba</link>
      <guid>https://dev.to/charliemorrison/13-telegram-bots-on-one-1gb-vps-the-real-ram-numbers-5ba</guid>
      <description>&lt;p&gt;Every thread about hosting a Telegram bot answers the same question with the same shrug. &lt;em&gt;How much server do I need?&lt;/em&gt; -- "not much", "a small VPS is fine", "1 GB is plenty". Nobody posts numbers. So people either overbuy a 4 GB instance for a bot that answers six commands, or they pick the cheapest box on the market and spend a weekend wondering whether it will fall over.&lt;/p&gt;

&lt;p&gt;I have thirteen Telegram bots running right now on a single 1 GB, 1 vCPU VPS that costs &lt;strong&gt;$4.17 a month&lt;/strong&gt;. Some are mine, some are client work, one is a staging copy. Instead of guessing, I measured every one of them. Here is what a Telegram bot actually costs in memory, what the real ceiling turned out to be, and the thing on my box that quietly used more RAM than all thirteen bots combined.&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%2Fqfazbp5f4u5zaa724k5d.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%2Fqfazbp5f4u5zaa724k5d.png" alt="Terminal output showing free -m, uptime and per-service resident memory for 13 active Telegram bot systemd units on a 1 GB VPS, totalling 185 MB" width="800" height="703"&gt;&lt;/a&gt; Live readings from the box, 1 August 2026. Client bot names replaced with generic labels; every number is unedited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually on the machine
&lt;/h2&gt;

&lt;p&gt;The host is a 1 vCPU / 1 GB / 24 GB NVMe instance. Linux reports 961 MB usable after firmware reservations, which is the number worth planning against -- not the 1024 you paid for. Thirteen &lt;code&gt;tg-*&lt;/code&gt; systemd services are active: a media-downloader bot, seven demo and sales bots for a Telegram SaaS product, four client bots, and one staging duplicate.&lt;/p&gt;

&lt;p&gt;All of them are Python, all use long polling rather than webhooks, and each one is a separate systemd unit with its own token and its own process. That last detail matters more than it sounds like it should, and I will come back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Telegram bot really costs in RAM
&lt;/h2&gt;

&lt;p&gt;Reading resident set size straight out of &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt; for each service's main PID:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Resident memory&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Media downloader&lt;/td&gt;
&lt;td&gt;21.0 MB&lt;/td&gt;
&lt;td&gt;Fetches and returns media files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS demo ×5&lt;/td&gt;
&lt;td&gt;16.6-17.8 MB&lt;/td&gt;
&lt;td&gt;Full menu, database, payments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales / signup bots&lt;/td&gt;
&lt;td&gt;16.5 MB&lt;/td&gt;
&lt;td&gt;Forms, notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client bot A&lt;/td&gt;
&lt;td&gt;16.2 MB&lt;/td&gt;
&lt;td&gt;Team workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client bot B&lt;/td&gt;
&lt;td&gt;10.1 MB&lt;/td&gt;
&lt;td&gt;Command-driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client bot C&lt;/td&gt;
&lt;td&gt;9.9 MB&lt;/td&gt;
&lt;td&gt;Command-driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client bot D&lt;/td&gt;
&lt;td&gt;7.4 MB&lt;/td&gt;
&lt;td&gt;File uploads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client D staging&lt;/td&gt;
&lt;td&gt;3.2 MB&lt;/td&gt;
&lt;td&gt;Idle duplicate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Thirteen bots, 185.0 MB total, averaging 14.2 MB each.&lt;/strong&gt; The whole fleet fits in under a fifth of a 1 GB machine.&lt;/p&gt;

&lt;p&gt;The spread is the interesting part. The lightest bot uses 3.2 MB and the heaviest uses 21.0 MB -- a 6.5× range -- and that gap has almost nothing to do with how many users each one serves. The staging bot at 3.2 MB and the client bot at 7.4 MB run the same framework as the 17 MB SaaS bots. What separates them is &lt;em&gt;what they import&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A bot that only parses text commands loads the Telegram library and little else. A bot that pulls in an HTTP client, a database driver, an image library and a payments SDK pays for every one of those at startup, whether a user ever triggers that code path or not. Python's memory floor is set at import time, not at request time. If you want a cheaper bot, the lever is your dependency list, not your user count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest caveat about these numbers.&lt;/strong&gt; RSS counts shared memory pages once per process. Thirteen Python processes on the same host share a lot -- the interpreter itself, libc, and any identical library versions -- so summing RSS &lt;em&gt;overstates&lt;/em&gt; the true combined footprint. The real figure is somewhat below 185 MB. I am quoting the pessimistic number deliberately: if you plan against it you will not be surprised, and it is the number you can reproduce yourself in one command. &lt;a href="https://docs.kernel.org/filesystems/proc.html" rel="noopener noreferrer"&gt;The kernel's proc documentation&lt;/a&gt; spells out what each field does and does not include. &lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest memory consumer was not a bot
&lt;/h2&gt;

&lt;p&gt;This is the part that changed how I think about small hosts. Sorting every process on the box by memory, the top entry is not a Telegram bot at all:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;Resident memory&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;warp-svc (networking daemon)&lt;/td&gt;
&lt;td&gt;162.8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node&lt;/td&gt;
&lt;td&gt;74.6 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;systemd-journal&lt;/td&gt;
&lt;td&gt;49.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All 13 Telegram bots combined&lt;/td&gt;
&lt;td&gt;185.0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A single networking daemon I installed once and forgot about uses 162.8 MB -- roughly &lt;strong&gt;88% of what all thirteen bots use together&lt;/strong&gt; , and about eleven average bots' worth of memory. Add &lt;code&gt;node&lt;/code&gt; and the journal and the non-bot overhead comfortably exceeds the entire fleet.&lt;/p&gt;

&lt;p&gt;So the mental model most people bring to this is backwards. When someone asks "can my 1 GB VPS handle another bot?", the honest answer is that one more bot costs about 14 MB and is almost never the problem. The thing to audit is everything on the box that &lt;em&gt;is not&lt;/em&gt; a bot: the monitoring agent, the VPN client, the container runtime, the log daemon with no retention limit. That is where a 1 GB machine actually goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real ceiling is not memory
&lt;/h2&gt;

&lt;p&gt;Here is the number I am not going to dress up. The box shows 687 MB of RAM in use and &lt;strong&gt;1041 MB of swap in use&lt;/strong&gt;. It is over-committed, and it has been for a while.&lt;/p&gt;

&lt;p&gt;It has also been up for 45 days with a load average of 0.52, 0.17, 0.05, and the media bot has recorded zero restarts. Nothing is falling over. What that combination means is that a pile of memory has been paged out to disk and is simply never touched again -- idle bots holding startup allocations they will not read a second time. On NVMe, that is a reasonable trade rather than a crisis, and it is exactly the behaviour Linux is supposed to produce.&lt;/p&gt;

&lt;p&gt;But it does define the actual limit. When the constraint arrives, it will show up as latency, not as an out-of-memory kill: a bot whose pages have been swapped out takes a beat longer to answer the first message after a quiet spell. On one vCPU, the thing to watch is several bots waking simultaneously -- a shared burst is a CPU and paging problem, never a headline RAM problem.&lt;/p&gt;

&lt;p&gt;Two practical consequences I would apply to any small box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap the journal.&lt;/strong&gt; An uncapped systemd journal grows until it owns real memory and real disk. Setting a size limit is a one-line change and buys back more than a bot's worth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set per-service memory limits.&lt;/strong&gt; systemd will do this for you with &lt;code&gt;MemoryMax=&lt;/code&gt; in the unit file, so one leaking bot degrades itself instead of the twelve next to it. The options are documented in &lt;a href="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html" rel="noopener noreferrer"&gt;systemd's resource-control manual&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One process per bot, and why it is worth it
&lt;/h2&gt;

&lt;p&gt;Thirteen separate services is not the most efficient arrangement available. I could run several bots in one process and share an interpreter, saving maybe 100 MB of the 185.&lt;/p&gt;

&lt;p&gt;I do not, and the reason is that the 100 MB is not the scarce resource -- my attention is. When a client bot crashes on a bad update, it crashes alone. systemd restarts that one unit, the other twelve never notice, and the journal tells me exactly which one it was. Sharing a process to save memory I am not short of would trade a resource I have for a failure mode I would have to debug at two in the morning.&lt;/p&gt;

&lt;p&gt;The same reasoning drives long polling over webhooks. Webhooks are more efficient at scale and need one HTTPS endpoint rather than thirteen open connections. But they also need a public certificate, a reverse proxy and a working DNS record before a single message is delivered -- and every one of those is a thing that can break independently. Long polling, described in the &lt;a href="https://core.telegram.org/bots/api#getupdates" rel="noopener noreferrer"&gt;official Telegram Bot API documentation&lt;/a&gt;, needs none of it: the bot dials out, so it works behind any firewall and needs no inbound access at all. At my volume that trade is obvious. Past a few hundred messages a second it flips, and &lt;a href="https://docs.python-telegram-bot.org/" rel="noopener noreferrer"&gt;python-telegram-bot's documentation&lt;/a&gt; covers both modes if you need to switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure your own box in one command
&lt;/h2&gt;

&lt;p&gt;Nothing above required special tooling. To get the same table for your own server:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemctl list-units 'tg-*' --state=active --no-legend --plain&lt;/code&gt; gives you the running units; for each one, &lt;code&gt;systemctl show -p MainPID --value &amp;lt;unit&amp;gt;&lt;/code&gt; gives the PID, and &lt;code&gt;grep VmRSS /proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt; gives the memory. Then run &lt;code&gt;ps -eo rss,comm --sort=-rss | head&lt;/code&gt; -- that second command is the one that matters, because it is what showed me the 162.8 MB daemon I would never have suspected.&lt;/p&gt;

&lt;p&gt;If you are sizing a box before you build anything: budget about 15 MB per bot, then add up everything else you intend to install, and let the second number drive the decision. On this evidence a 1 GB instance holds well over thirty typical bots on memory alone. It will run out of CPU, patience, or forgotten background daemons long before it runs out of RAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  See a Mini App that needs no server at all
&lt;/h3&gt;

&lt;p&gt;The cheapest bot to host is the one with no backend. My party game ships its logic inside the page Telegram opens -- 151 questions per language, English and Ukrainian, no signup, nothing to switch off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://t.me/charlie_party_bot/partygame?startapp=vpsram" rel="noopener noreferrer"&gt;Open the Party Game&lt;/a&gt; No Telegram? It runs in a browser too: &lt;a href="https://charliemorrison.dev/party-game/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=13-telegram-bots-1gb-vps" rel="noopener noreferrer"&gt;charliemorrison.dev/party-game&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Not here to run a server?
&lt;/h3&gt;

&lt;p&gt;If you landed on this because you wanted a game night rather than a hosting bill, &lt;strong&gt;The Telegram Party Pack&lt;/strong&gt; skips the infrastructure entirely: 255 prompts across six games, rewritten for a group chat, plus a host guide covering polls, spoiler reveals and keeping a round legible when replies arrive out of order. Files you own -- no server, no uptime, nothing to maintain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/buy/c7bd4341-6eb3-4acc-b8e1-7946e1413b98?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=13-telegram-bots-1gb-vps" rel="noopener noreferrer"&gt;Get the pack -- $9.99&lt;/a&gt; The Mini App above stays free, no signup. See what's in the pack before you buy.&lt;/p&gt;

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

&lt;p&gt;How much RAM does a Telegram bot need?&lt;/p&gt;

&lt;p&gt;On this server a long-polling Python bot settles between 3 MB and 21 MB resident, averaging 14.2 MB across 13 bots. The variation tracks what the bot imports rather than how many users it has -- an HTTP client, a database driver and a media library cost more at startup than any amount of traffic does at runtime.&lt;/p&gt;

&lt;p&gt;Can I run multiple Telegram bots on one 1 GB VPS?&lt;/p&gt;

&lt;p&gt;Yes. Thirteen bots here use 185 MB in total, under a fifth of the machine. Memory is rarely the binding constraint -- a single vCPU during simultaneous bursts, and non-bot daemons, both bite first.&lt;/p&gt;

&lt;p&gt;Does each bot need its own server?&lt;/p&gt;

&lt;p&gt;No. Each needs its own process and token, but one host handles many. A separate systemd service per bot on one machine gives you crash isolation without paying for separate servers.&lt;/p&gt;

&lt;p&gt;Long polling or webhooks?&lt;/p&gt;

&lt;p&gt;Long polling is cheaper and much simpler: no public endpoint, no certificate, no reverse proxy, and it works behind any firewall because the bot dials out. Webhooks win when you have high volume and already run a web server. At thirteen low-traffic bots, polling is not close to being the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Telegram download bots in 2026 -- the media bot at the top of that memory table, and what it actually does.&lt;/li&gt;
&lt;li&gt;I messaged 18 "best" Telegram game bots -- 7 never replied -- what happens when nobody pays the $4.17.&lt;/li&gt;
&lt;li&gt;All the bots and Mini Apps I run -- including the ones I retired, marked as retired.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/13-telegram-bots-1gb-vps/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>telegram</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Sent 24 Requests to Telegram's Bot API to Find What Breaks a Group Game</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Wed, 29 Jul 2026 20:19:19 +0000</pubDate>
      <link>https://dev.to/charliemorrison/i-sent-24-requests-to-telegrams-bot-api-to-find-what-breaks-a-group-game-2lmg</link>
      <guid>https://dev.to/charliemorrison/i-sent-24-requests-to-telegrams-bot-api-to-find-what-breaks-a-group-game-2lmg</guid>
      <description>&lt;p&gt;Every article about Telegram games answers the same question: &lt;em&gt;which&lt;/em&gt; games. Truth or Dare, Never Have I Ever, Would You Rather, Most Likely To. Fine: that part is easy, and I've written one of those lists myself.&lt;/p&gt;

&lt;p&gt;Nobody writes about the part that actually decides whether your game night survives past round three: &lt;strong&gt;the chat mechanics&lt;/strong&gt;. A group chat is not a living room. Nobody takes turns. The person who types fastest answers first and quietly sets the answer everyone else copies. Half the group is reading on a phone under a table. The host, meaning you, is trying to run a game in a medium that has no concept of "whose turn it is".&lt;/p&gt;

&lt;p&gt;Telegram has real tools for this: polls, quiz polls, spoiler text, pinned messages, self-closing rounds. Most of what's written about them is copied from other articles, and a fair amount of it is simply out of date. So rather than trust it, I sent &lt;strong&gt;24 live requests to Telegram's own API&lt;/strong&gt; : real polls, real spoilers, real pins, and I wrote down exactly what came back.&lt;/p&gt;

&lt;p&gt;Seven of the 24 failed. Two of them "succeeded" while doing something other than what I asked, which is worse, because those are the two you ship to your friends without noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;I run a party game on Telegram, so the questions weren't hypothetical: I wanted to know which of these mechanics I could rely on when a group of eight people is mid-round and impatient.&lt;/p&gt;

&lt;p&gt;The method was deliberately boring. Each mechanic became one request to &lt;a href="https://core.telegram.org/bots/api#sendpoll" rel="noopener noreferrer"&gt;Telegram's Bot API&lt;/a&gt;, using &lt;code&gt;sendPoll&lt;/code&gt;, &lt;code&gt;sendMessage&lt;/code&gt; and &lt;code&gt;pinChatMessage&lt;/code&gt;, with one variable changed at a time: a poll question at 300 characters and then at 301, an option at 100 and then at 101, a quiz with a correct answer and then without one. Whatever the API returned, success or error, went straight into a results file. The screenshot below is generated from that file, not typed by hand.&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%2Fm6y61zdgsirbvl5kqv5t.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%2Fm6y61zdgsirbvl5kqv5t.png" alt="Results table of 24 live requests to the Telegram Bot API, showing which game-night mechanics were accepted and which were rejected, with the API's own error messages" width="800" height="814"&gt;&lt;/a&gt; All 24 requests. Green rows were accepted; red rows carry Telegram's own error text. The two red rows with code 200 are the dangerous ones.&lt;/p&gt;

&lt;p&gt;One thing I could not test, and won't pretend otherwise: the per-group rate limit. My test account is restricted from creating new groups, so the requests went to a one-to-one chat with my bot, where 22 back-to-back messages went through in 6.1 seconds without a single block. That number does not transfer to a group. Telegram's own &lt;a href="https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this" rel="noopener noreferrer"&gt;bot FAQ&lt;/a&gt; puts the guidance at roughly 20 messages per minute in the same group, and everything I say about pacing below follows their figure, not mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five things that break a round
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The 100-character option cap hits the exact game you most want to poll
&lt;/h3&gt;

&lt;p&gt;A poll question can be 300 characters. Each option can only be 100. I got &lt;code&gt;poll options length must not exceed 100&lt;/code&gt; at 101 characters, and success at exactly 100.&lt;/p&gt;

&lt;p&gt;That sounds generous until you look at what you're actually pasting. I ran all 255 prompts from my own game pack through those two limits. Zero exceeded the question cap. Twenty exceeded the option cap, and &lt;strong&gt;19 of those 20 were Would You Rather&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which makes sense once you see it. A Would You Rather line is already a question &lt;em&gt;plus&lt;/em&gt; its two answers glued together: "Would you rather have unlimited money but no free time, or unlimited free time but just enough money?" runs to 138 characters. Paste that whole line as an option and Telegram rejects it. Would You Rather is the game that maps most naturally onto a poll, and it's the one whose text most reliably breaks the poll.&lt;/p&gt;

&lt;p&gt;The fix is a split, not an edit. The dilemma goes in the poll &lt;em&gt;question&lt;/em&gt; (300 characters, so it always fits; my longest was 101). The two halves become the two options: "Money, no time" / "Time, no money". When I re-ran the same 30 Would You Rather prompts that way, &lt;strong&gt;all 30 fit&lt;/strong&gt; , without cutting a single word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Host shortcut:&lt;/strong&gt; if a prompt is too long to be an option, it was never an option. It was a question with the answers written into it. Split it, don't trim it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The option limit is 12, not the 10 you'll read everywhere
&lt;/h3&gt;

&lt;p&gt;Twelve options were accepted and created normally. Thirteen came back &lt;code&gt;poll can't have more than 12 options&lt;/code&gt;. A lot of current advice still says ten, which was true of an earlier version of the API and quietly stopped being true.&lt;/p&gt;

&lt;p&gt;This matters for exactly one thing, and it's the most popular game in the genre: Most Likely To. You want one option per player, so the group can vote for each other. Twelve options means a group of up to twelve fits in a single poll. At thirteen people you either split the vote across two polls, which fractures it, or drop the poll and take answers as replies.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Quiz mode demands a right answer, so it's wrong for opinion games
&lt;/h3&gt;

&lt;p&gt;A quiz poll without a designated correct option is rejected: &lt;code&gt;correct quiz option list must be non-empty&lt;/code&gt;. With one, it works, and you can attach an explanation that pops up after the vote.&lt;/p&gt;

&lt;p&gt;So quiz mode is for trivia, where there genuinely is a correct answer, and the explanation slot is the best feature nobody uses: it lets the poll teach instead of just scoring. For anything with no right answer (Would You Rather, Most Likely To, Never Have I Ever) you want a regular poll. Reach for quiz mode there and Telegram forces you to declare one of your friends' opinions officially correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Polls are anonymous by default, which silently kills Most Likely To
&lt;/h3&gt;

&lt;p&gt;Anonymity is the default, and that default is exactly backwards for half these games.&lt;/p&gt;

&lt;p&gt;Never Have I Ever wants anonymity. People answer honestly precisely because nobody sees who tapped "I have". Most Likely To wants the opposite: the accusation &lt;em&gt;is&lt;/em&gt; the game. "Most likely to reply at 3am: Ann, Bo, Cy" is only funny if you can see that four people voted for Bo. Run it anonymously and you get a bar chart, which nobody laughs at.&lt;/p&gt;

&lt;p&gt;Turning anonymity off is one toggle when you create the poll, and it's the single highest-value decision in the whole session. My rule: &lt;strong&gt;confessions anonymous, accusations public.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Two requests that succeed while doing the wrong thing
&lt;/h3&gt;

&lt;p&gt;These are the two red rows carrying code 200, and they're the reason I ran this test at all. An outright rejection you notice immediately, a silent substitution you ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoiler formatting is ignored inside a poll question.&lt;/strong&gt; Telegram &lt;a href="https://telegram.org/blog/reactions-spoilers-translations" rel="noopener noreferrer"&gt;introduced spoiler text in 2021&lt;/a&gt;, and it's the mechanic that finally makes hidden-answer games work in a chat: the answer sits there covered until someone taps it. Write a poll question as &lt;code&gt;Guess: ||Bo||&lt;/code&gt; expecting the name to be hidden, and the API returns success, then delivers the question with the pipes visible as ordinary characters. The answer isn't hidden. It's printed, in a slightly uglier font, to everyone.&lt;/p&gt;

&lt;p&gt;Spoilers only work in ordinary messages. So the pattern is two messages, not one: poll first, reveal second, spoiler applied to the reveal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bots cannot schedule anything.&lt;/strong&gt; I sent a message with a scheduling timestamp one hour in the future. The API said success and delivered it instantly. My "scheduled" message arrived 3,599 seconds before its own schedule. There is no scheduling parameter for bots; the API just ignores what it doesn't recognise instead of telling you.&lt;/p&gt;

&lt;p&gt;Scheduling does exist, in the Telegram app, on your side: compose the message, hold the send button, pick a time. That's how you have the rules card land in the group at 20:00 without being awake for it. It is not something you can automate through a bot, and if a tool promises you that, it is doing something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two mechanics worth adding on purpose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Self-closing rounds.&lt;/strong&gt; A poll can carry an open period. I set 30 seconds and it was created with a 30-second life. This is the closest Telegram gets to a turn timer. It ends the round for you, so the game doesn't die of one person "answering later", and it applies pressure that makes answers funnier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin the rules, then edit the pin.&lt;/strong&gt; Pinning worked, and so did editing the text of an already-sent message. Together those are a live scoreboard: pin one message at the start, edit it as the night goes, and anyone arriving late reads the current state instead of scrolling. Editing a pin doesn't re-notify the group, so it's cheap to update often.&lt;/p&gt;

&lt;p&gt;One thing the API will not do is protect you from a broken round. A poll with a &lt;em&gt;single&lt;/em&gt; option was accepted without complaint: a poll nobody can meaningfully vote in, delivered with a cheerful success code. Telegram checks lengths and counts. It does not check whether your game makes sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  The host protocol this adds up to
&lt;/h2&gt;

&lt;p&gt;Strip out the API detail and what's left is a short list of things a host does, all of which exist because the medium has no turns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name one host per session.&lt;/strong&gt; Not a rotation, not a democracy. One person sends the prompts and closes the rounds, because otherwise two people post round four simultaneously and the thread splits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the rules, including the pace.&lt;/strong&gt; "One round every ten minutes, replies by reply-thread." Half the failures aren't disagreement, they're people guessing at the tempo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll first, talk second.&lt;/strong&gt; A poll captures everyone's answer before the loudest voice anchors it. In an open-text round the first reply becomes the template for every reply after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two passes each, then the round closes.&lt;/strong&gt; The single rule that keeps quiet people in the game. It caps the extroverts without singling anyone out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Space the sends.&lt;/strong&gt; Around 20 messages per minute to one group, per Telegram's guidance. Twelve prompts fired back to back is also just unreadable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spoiler the reveals.&lt;/strong&gt; Separate message, spoiler applied there, never in the poll question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is clever. It's just the set of things that turn out to be load-bearing once you stop assuming a group chat behaves like a room.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try the free game first
&lt;/h3&gt;

&lt;p&gt;Truth or Dare, Never Have I Ever and Would You Rather, with 151 questions per language, English and Ukrainian, no signup and no backend that can be switched off. Open it in the chat and start a round.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://t.me/charlie_party_bot/partygame?startapp=hostguide" rel="noopener noreferrer"&gt;Open the Party Game&lt;/a&gt; No Telegram? It runs in a browser too: &lt;a href="https://charliemorrison.dev/party-game/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=how-to-run-a-telegram-game-night" rel="noopener noreferrer"&gt;charliemorrison.dev/party-game&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Want the rounds already built for polls?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Telegram Party Pack&lt;/strong&gt; is the version of this with the work done: 255 prompts across six games, each written to fit inside Telegram's limits, plus poll-ready files where the Would You Rather dilemmas are already split into question and options, the exact split this test says you need. Includes the host guide: pinned rules, pacing, spoiler reveals and keeping a round legible when replies arrive out of order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/buy/c7bd4341-6eb3-4acc-b8e1-7946e1413b98?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=telegram&amp;amp;utm_content=how-to-run-a-telegram-game-night" rel="noopener noreferrer"&gt;Get the pack — $9.99&lt;/a&gt; The game above stays free, no signup.&lt;/p&gt;

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

&lt;p&gt;How many options can a Telegram poll have?&lt;/p&gt;

&lt;p&gt;Twelve. Tested directly: 12 options were created normally, 13 came back &lt;code&gt;poll can't have more than 12 options&lt;/code&gt;. Plenty of guides still say ten, which was true of an older API version. The question is capped at 300 characters and each option at 100.&lt;/p&gt;

&lt;p&gt;Why can't I see who voted in my Telegram poll?&lt;/p&gt;

&lt;p&gt;Because polls are anonymous unless you say otherwise, and most people never change it. For accusation games like Most Likely To that's fatal, because seeing who picked whom is the game. Turn anonymity off for those, leave it on for confession-style rounds where hiding the vote is what makes people honest.&lt;/p&gt;

&lt;p&gt;Can I schedule game-night messages from a bot?&lt;/p&gt;

&lt;p&gt;No. The Bot API has no scheduling parameter and doesn't complain if you invent one. My message with a timestamp an hour ahead was delivered immediately with a success code. Scheduling lives in the Telegram apps: compose, hold send, pick a time.&lt;/p&gt;

&lt;p&gt;Do spoiler tags work inside a poll question?&lt;/p&gt;

&lt;p&gt;No. Formatting is ignored in poll questions, so &lt;code&gt;||spoiler||&lt;/code&gt; arrives with the pipes visible and nothing hidden. Send the poll, then reveal the answer in a separate message with the spoiler applied there.&lt;/p&gt;

&lt;p&gt;Which games work best as polls, and which as plain messages?&lt;/p&gt;

&lt;p&gt;Polls suit anything with fixed choices: Would You Rather (split into question and options), Most Likely To (one option per player, up to twelve), Never Have I Ever (two options, kept anonymous). Plain messages suit anything open-ended, such as Truth or Dare, Story Chain and Paranoia, where the answer is written, not chosen, and the spoiler reveal does the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Telegram party games and bots in 2026: which games to play, if you're still choosing.&lt;/li&gt;
&lt;li&gt;I messaged 18 "best" Telegram game bots — 7 never replied: why I stopped recommending bots you can't verify.&lt;/li&gt;
&lt;li&gt;The Telegram tech quiz game: quiz mode used the way this test says it should be.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/how-to-run-a-telegram-game-night/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>telegram</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>I Messaged 18 'Best' Telegram Game Bots. 7 Never Replied</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:21:54 +0000</pubDate>
      <link>https://dev.to/charliemorrison/i-messaged-18-best-telegram-game-bots-7-never-replied-2a1b</link>
      <guid>https://dev.to/charliemorrison/i-messaged-18-best-telegram-game-bots-7-never-replied-2a1b</guid>
      <description>&lt;p&gt;Search for something like "best Telegram games" and you'll get a dozen confident listicles. Each one hands you a set of &lt;code&gt;@handles&lt;/code&gt;: Hangman, 2048, Werewolf, Poker, Snake. What none of them tell you is which of those bots is still &lt;em&gt;running&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's not a nitpick. I run a Telegram game myself, and in May 2026 I switched off the chat-command backend behind it. The bot account stayed. Its profile page kept loading. My own article kept telling readers to add it to a group and type &lt;code&gt;/play&lt;/code&gt; — into total silence, for weeks, before I noticed. Nobody complained, because a dead bot doesn't produce a complaint. It produces nothing.&lt;/p&gt;

&lt;p&gt;So I stopped guessing and measured it. I took every bot recommended by three "best Telegram games" articles currently ranking on Google, sent each one a real &lt;code&gt;/start&lt;/code&gt; from a real Telegram account, and waited to see what came back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eleven answered. Seven said nothing at all.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the test worked
&lt;/h2&gt;

&lt;p&gt;I wanted this to be boring and reproducible rather than clever, so the method is exactly what a person would do by hand — just automated so all 18 got identical treatment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sample&lt;/strong&gt; is the union of the bots recommended by three articles ranking for "best Telegram game bots" in July 2026: a 16-game roundup on LightXtremeVPN, MakeUseOf's "8 Fun Telegram Game Bots You Should Try", and Membertel's "5 best Telegram Game Bots". Deduplicated, that's 18 distinct handles. I didn't hand-pick them and I didn't drop any.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The probe&lt;/strong&gt; is one message: &lt;code&gt;/start&lt;/code&gt;, the command every Telegram bot is expected to handle, sent from my own account (&lt;a class="mentioned-user" href="https://dev.to/charliemorrison"&gt;@charliemorrison&lt;/a&gt;) rather than a fresh burner, so nothing could be blamed on an unusual account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wait&lt;/strong&gt; is 15 seconds. That is extremely generous — a live bot replies in well under a second, because the reply is a program responding, not a person typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verdict&lt;/strong&gt; is based on new inbound messages only. I record the newest message ID in the chat &lt;em&gt;before&lt;/em&gt; sending, then count only replies newer than that. Otherwise an old message from a previous visit would make a dead bot look alive — which, on two of these chats, it would have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pacing:&lt;/strong&gt; 6–11 seconds of randomised delay between bots, so the run looks like a person poking around rather than a flood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the actual run:&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%2Fkww0yjxuyoo56armuq20.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%2Fkww0yjxuyoo56armuq20.png" alt="Terminal output listing all 18 Telegram game bots with the result of sending /start to each: 11 answered, 7 silent" width="800" height="549"&gt;&lt;/a&gt; The real output. 15 seconds of patience per bot, 18 bots, no cherry-picking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seven that said nothing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;th&gt;Listed as&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@PlayGame2048Bot&lt;/td&gt;
&lt;td&gt;2048 Puzzle&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Snakeslite_official_bot&lt;/td&gt;
&lt;td&gt;SnakeLite&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@HangBot&lt;/td&gt;
&lt;td&gt;Hangman&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@dreamersbot&lt;/td&gt;
&lt;td&gt;Dreamers&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@ArenaGameTelegramBot&lt;/td&gt;
&lt;td&gt;Arena Game RPG&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@chessybot&lt;/td&gt;
&lt;td&gt;Chessy&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@LumberjackBot&lt;/td&gt;
&lt;td&gt;Lumberjack&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of those handles resolves. Open any of them in Telegram and you get a normal-looking bot profile with a name and a Start button. Press Start and the chat stays empty. There is no error, no "this bot is no longer available", no hint that you're talking to a switched-off machine. The most common reaction to that is to assume &lt;em&gt;you&lt;/em&gt; did something wrong.&lt;/p&gt;

&lt;p&gt;Worth flagging: &lt;strong&gt;Lumberjack is a special case that shows exactly how these lists rot.&lt;/strong&gt; The standalone @LumberjackBot is silent — but LumberJack the game is still perfectly playable, because it lives inside @gamebot, which answered immediately. One article recommends the working route, another recommends a handle that leads nowhere, and both describe the same game. If you followed the second one you'd conclude the game is gone. It isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eleven that answered
&lt;/h2&gt;

&lt;p&gt;These replied within the window, most of them instantly: @gamebot, @ChessBot, @xoBot, @unobot, @QuizariumBot, @wordibot, @RatherGameBot, @werewolfbot, @chat_against_humanity_bot, @PokerBot and @Gamee.&lt;/p&gt;

&lt;p&gt;Two details are worth having before you pick one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Several are group-only, and that surprises people.&lt;/strong&gt; Quizarium's reply to a private &lt;code&gt;/start&lt;/code&gt; is: &lt;em&gt;"Yes, it's my command, but you can use it only within a group chat."&lt;/em&gt; Chat Against Humanity says the same in different words — you need a group and a couple of friends before anything happens. Not broken, just not a solo experience, which most roundups fail to mention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@unobot's own first message points somewhere else:&lt;/strong&gt; &lt;em&gt;"Also check out @UnoDemoBot, a newer version of this bot with exclusive modes and features."&lt;/em&gt; The bot itself is telling you the recommendation is a version behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat I'd rather state than hide: this is a snapshot taken on 28 July 2026. A bot that answered today can go quiet next month — that is the entire point of the exercise. Treat the list as a demonstration of the method, not as a permanent verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bots die quietly
&lt;/h2&gt;

&lt;p&gt;The reason is structural, and once you see it you can predict which recommendations will rot.&lt;/p&gt;

&lt;p&gt;A Telegram bot is not a self-contained thing living on Telegram's servers. It's the Telegram-facing half of somebody's software. The platform's own documentation is blunt about the other half: &lt;a href="https://core.telegram.org/bots/faq" rel="noopener noreferrer"&gt;"In order for a bot to work, set up a bot account with @BotFather, then connect it to your backend server via our API."&lt;/a&gt; That backend is a machine someone rents, maintains and pays for every month.&lt;/p&gt;

&lt;p&gt;When the developer moves on — new job, lost interest, hosting bill no longer worth it — they stop paying for the server. They almost never delete the bot account, because deleting it takes deliberate effort and produces no benefit. So the account outlives the product. Telegram keeps rendering the profile from its own records, and your Start button keeps sending commands into a machine that is no longer there.&lt;/p&gt;

&lt;p&gt;This isn't unique to Telegram, it's just unusually invisible there. Pew Research Center's 2024 study on digital decay found that &lt;a href="https://www.pewresearch.org/data-labs/2024/05/17/when-online-content-disappears/" rel="noopener noreferrer"&gt;"a quarter of all webpages that existed at one point between 2013 and 2023 are no longer accessible"&lt;/a&gt;, and that 38% of pages from 2013 are gone. On the web, decay at least announces itself with a 404. A dead bot has no 404. It has an empty chat.&lt;/p&gt;

&lt;p&gt;Which means &lt;strong&gt;39% silent in this sample isn't a scandal — it's roughly what a decade of link rot looks like when it's applied to software that nobody can see rotting.&lt;/strong&gt; The scandal is that lists keep recommending them without ever pressing Start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug in my own test that nearly produced a fake result
&lt;/h2&gt;

&lt;p&gt;I'll include this because it's the most useful thing I learned, and because the first version of this article would have been wrong.&lt;/p&gt;

&lt;p&gt;My first run died four bots in. Everything after @ChessBot came back with the same error — &lt;code&gt;TypeNotFoundError: Could not find a matching Constructor ID&lt;/code&gt; — and the naive reading was that 12 bots were unreachable. That would have been a much more dramatic headline and completely false.&lt;/p&gt;

&lt;p&gt;What actually happened: my client library (Telethon 1.43.2) was receiving live account updates in the background, including an unrelated spam broadcast that used a newer Telegram data type than the library knows about. One unparseable object killed the connection's receive loop, and every later call inherited the corpse. The bots were fine. My tooling was lying to me.&lt;/p&gt;

&lt;p&gt;The fix was one argument — &lt;code&gt;receive_updates=False&lt;/code&gt;, so the server never pushes those updates to this client at all — and re-running gave clean results for 15 of 18. The last three still errored, so I re-probed those with a fresh connection each. All three came back conclusive: @Gamee alive, @chessybot and @LumberjackBot genuinely silent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The transferable lesson:&lt;/strong&gt; when a tool reports a wall of identical failures, suspect the tool before you believe the finding. A real-world result is usually messy — some pass, some fail. Perfectly uniform failure, starting at an arbitrary point in the run, is the signature of something breaking in &lt;em&gt;your&lt;/em&gt; code, not of the world suddenly agreeing to be broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15-second check you can do yourself
&lt;/h2&gt;

&lt;p&gt;You don't need any of my scripts. Before you trust a bot from any list, including mine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the bot and press &lt;strong&gt;Start&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Count to fifteen.&lt;/li&gt;
&lt;li&gt;If nothing arrived, the backend is off. Move on — there is nothing to fix on your end.&lt;/li&gt;
&lt;li&gt;If it's a group game, add it to a group and run its start command there before inviting people. Several of these only wake up in groups.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one thing that proves nothing is the bot's profile loading. Every dead bot in this test has a perfectly healthy profile page. Telegram serves that from its own database; it has no idea whether the developer's server is still switched on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changed about the game I run
&lt;/h2&gt;

&lt;p&gt;Watching my own bot go silent, then measuring how normal that is, is why my party game no longer depends on a backend at all. It's a Mini App: the questions and the game logic ship inside the page Telegram opens, so there is no server left to switch off. It also opens in a normal browser, which means it survives even if I lose interest entirely — the failure mode that killed seven bots above simply doesn't exist for it.&lt;/p&gt;

&lt;p&gt;That's not a claim you should take on faith after an article about not taking claims on faith. Press Start and count to fifteen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test it yourself — it should answer instantly
&lt;/h3&gt;

&lt;p&gt;Truth or Dare, Never Have I Ever and Would You Rather. 151 questions per language, English and Ukrainian, no signup, no backend to die.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://t.me/charlie_party_bot/partygame?startapp=liveness" rel="noopener noreferrer"&gt;Open the Party Game&lt;/a&gt; No Telegram? It runs in a browser too: charliemorrison.dev/party-game&lt;/p&gt;

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

&lt;p&gt;Why does a Telegram bot stop replying?&lt;/p&gt;

&lt;p&gt;Because a bot is only the Telegram-facing half of the product. Telegram's documentation tells developers to connect a bot account to their backend server. When that server is switched off, unpaid for or abandoned, the bot account still exists and its profile still loads — but nothing answers your commands.&lt;/p&gt;

&lt;p&gt;How do I tell if a bot is dead before trusting a recommendation?&lt;/p&gt;

&lt;p&gt;Open it, press Start, wait about 15 seconds. A working bot answers almost instantly because the reply is automated. If nothing arrives, the backend isn't running. A profile page that loads normally proves nothing.&lt;/p&gt;

&lt;p&gt;Do dead Telegram bots show an error message?&lt;/p&gt;

&lt;p&gt;No, and that's what makes them hard to spot. No error, no warning, no retirement notice — you press Start and the chat stays empty, which most people read as their own mistake.&lt;/p&gt;

&lt;p&gt;Which recommended game bots still work?&lt;/p&gt;

&lt;p&gt;In this test, 11 of 18: GameBot, ChessBot, xoBot, unobot, QuizariumBot, wordibot, RatherGameBot, werewolfbot, chat_against_humanity_bot, PokerBot and Gamee. The seven silent ones were PlayGame2048Bot, Snakeslite_official_bot, HangBot, dreamersbot, ArenaGameTelegramBot, chessybot and LumberjackBot. Snapshot from 28 July 2026 — re-check before you rely on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Telegram party games and bots in 2026 — the roundup this test came out of, including why chat bots and Mini Apps fail differently.&lt;/li&gt;
&lt;li&gt;The Telegram tech quiz game — the same no-backend approach applied to a quiz.&lt;/li&gt;
&lt;li&gt;All the bots and Mini Apps I run — including the two I retired, marked as retired instead of quietly left up.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/i-messaged-18-telegram-game-bots/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance. It links to a free tool I built and run myself; there is no paid tier and no affiliate link in this post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>telegram</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Generated a Month of Social Posts With One Free Tool — Here's the Mix That Held Up</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Tue, 21 Jul 2026 19:59:38 +0000</pubDate>
      <link>https://dev.to/charliemorrison/i-generated-a-month-of-social-posts-with-one-free-tool-heres-the-mix-that-held-up-2nc</link>
      <guid>https://dev.to/charliemorrison/i-generated-a-month-of-social-posts-with-one-free-tool-heres-the-mix-that-held-up-2nc</guid>
      <description>&lt;p&gt;Here is how most small accounts actually plan their social media: they don't. They open the app in the morning, remember they have a thing to sell, and post about the thing. The next day they feel guilty for being salesy, so they post nothing. Three days later a competitor does something and they fire off a reactive post. That is not a strategy, it is a mood, and the algorithm can smell the difference. The accounts that grow are boring in exactly one way: they show up with a &lt;em&gt;plan&lt;/em&gt; , and the plan is mostly not about them.&lt;/p&gt;

&lt;p&gt;I wanted to see what that plan looks like when you take the human indecision out of it. So instead of writing about content-calendar theory, I ran a real niche through a &lt;a href="https://charliemorrison.dev/content-calendar-generator?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=social&amp;amp;utm_content=i-generated-a-month-of-social-posts" rel="noopener noreferrer"&gt;free content calendar generator&lt;/a&gt; I put on this site, read every post it planned, and checked whether the balance it produced matches what the people who study this for a living recommend. This is that experiment: one frozen input, the raw output, and an honest account of what a machine gets right and wrong about a month of posting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input I froze
&lt;/h2&gt;

&lt;p&gt;I gave it a deliberately unglamorous, real-world case -- the kind of account that actually struggles with this: a small &lt;strong&gt;fitness coaching&lt;/strong&gt; business on &lt;strong&gt;Instagram&lt;/strong&gt; , posting &lt;strong&gt;5 times a week&lt;/strong&gt; for &lt;strong&gt;4 weeks&lt;/strong&gt;. Twenty posts. No brand voice tuning, no cherry-picking; I took whatever the first generation handed me and read it top to bottom.&lt;/p&gt;

&lt;p&gt;The first thing I noticed was what it refused to do. I never told it "go easy on the selling." It decided that on its own. Out of twenty planned posts, only three were straight promotion. The other seventeen were teaching, asking, showing behind-the-scenes, or proving results -- the stuff that earns the right to sell in the first place.&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%2Fgykw7zexzpni45rpjgyl.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%2Fgykw7zexzpni45rpjgyl.png" alt="Screenshot of the content calendar generator output showing a balanced mix of educate, engage, behind-the-scenes, inspire and promote posts for a fitness niche on Instagram" width="800" height="1244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generated calendar for a fitness coach on Instagram -- each card carries a pillar badge, a hook, a CTA, and a best-time suggestion. Note how few of them are "Promote."&lt;/p&gt;

&lt;h2&gt;
  
  
  The mix it enforced (and why it's right)
&lt;/h2&gt;

&lt;p&gt;When I tallied the pillar badges across the twenty posts, the shape was clear. Educational content was the backbone, engagement posts came second, and promotion was deliberately a minority slice. Roughly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Share of the month&lt;/th&gt;
&lt;th&gt;What it looked like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Educate&lt;/td&gt;
&lt;td&gt;~35%&lt;/td&gt;
&lt;td&gt;"3 mobility drills before you squat," "why your scale lies to you"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engage&lt;/td&gt;
&lt;td&gt;~20%&lt;/td&gt;
&lt;td&gt;"What's the one exercise you avoid? Be honest." polls, this-or-that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behind-the-scenes&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;"My actual 6am prep," a client session setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspire / proof&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;a transformation, a client win, a mindset reframe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Promote&lt;/td&gt;
&lt;td&gt;~15%&lt;/td&gt;
&lt;td&gt;the coaching offer, a spot opening, a free consult&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If that ratio feels familiar, it should. It is a near-exact match for the frameworks marketers have converged on for years. The classic &lt;a href="https://www.orbitmedia.com/blog/social-media-rule-of-thirds/" rel="noopener noreferrer"&gt;social media rule of thirds&lt;/a&gt; splits your feed into roughly one-third promotion, one-third curated industry value, and one-third personal, relationship-building content. The even more common &lt;a href="https://ansira.com/blog/the-80-20-rule/" rel="noopener noreferrer"&gt;80/20 rule&lt;/a&gt; is blunter: 80% of your posts should inform, educate, or entertain, and only 20% should sell. The generator landed at about 15% promotion without being told either rule -- it just bakes the principle in so you can't drift back to selling every day.&lt;/p&gt;

&lt;p&gt;That drift is the entire problem the tool solves. Nobody plans to be a walking advertisement. It happens because promotion is the only post type that feels "productive" in the moment, so when you improvise, you over-index on it. A calendar quietly removes the improvisation. You are no longer deciding &lt;em&gt;whether&lt;/em&gt; today is a sales day; the plan already decided, and it said no most of the time.&lt;/p&gt;

&lt;p&gt;The cost of getting this wrong is invisible until it isn't. An account that sells in half its posts doesn't get a warning; it just slowly trains its audience that opening a notification means being pitched. People mute before they unfollow, so you never see the moment they check out -- you only see reach quietly sliding down over months while you post the same amount. The value-first mix is not a feel-good nicety. It is the thing that keeps the audience willing to see the 15% of posts where you actually ask for the sale. You earn the promote posts with the seventeen that came before them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the generated posts actually contained
&lt;/h2&gt;

&lt;p&gt;A pillar label is worthless if the post underneath it is filler, so I read the contents, not just the badges. Each card came with four things I would otherwise have to invent from scratch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A specific idea&lt;/strong&gt; , not a category. Not "post something educational" but "the three warm-up mistakes I see every new client make." The difference between a prompt and a blank page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hook&lt;/strong&gt; written to survive the half-second thumb-scroll -- a question, a mild contradiction, or a number. Hooks are where most amateur posts die, and it is the one line I am worst at writing under time pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A platform-tuned CTA.&lt;/strong&gt; Instagram got "save this for your next session" and "DM me the word START," not the LinkedIn-flavored "thoughts?" that reads as tone-deaf on IG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A best-time-to-post nudge and format hint&lt;/strong&gt; (reel vs carousel vs single image), so the calendar is a shooting schedule, not just a topic list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Were they perfect, finished captions? No. They are strong first drafts -- the skeleton and the hook, with room for your real voice and your real client stories. But going from "I have no idea what to post for four weeks" to "I have twenty specific, balanced ideas with hooks" in about ten seconds is the entire value. The blank page is the tax; the tool pays it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do
&lt;/h2&gt;

&lt;p&gt;I would be lying if I sold this as a full content team. Three honest limits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't know your voice or your stories.&lt;/strong&gt; The ideas are sound and the structure is right, but the thing that makes a fitness post &lt;em&gt;yours&lt;/em&gt; is the client who cried after their first pull-up, and the tool has never met her. You still have to pour the specifics in. It gives you the mold; you supply the metal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't watch what works.&lt;/strong&gt; A real calendar is a loop: post, read the analytics, do more of what landed. A generator gives you a strong month one. It cannot tell you that your carousels crush and your talking-head reels flop, because it never sees your numbers. After the first cycle, you are the one who has to prune and double down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't replace consistency.&lt;/strong&gt; This is the quiet punchline of every content-mix framework, and the marketers who study it say it plainly: the ratio only works if you actually apply it week after week. A perfect calendar you post from for nine days and then abandon loses to a mediocre one you keep for six months. The tool removes the planning excuse. It cannot remove the showing-up part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest verdict
&lt;/h2&gt;

&lt;p&gt;For anyone staring at an empty content calendar -- a solo founder, a freelancer, a small brand with no social hire -- generating a balanced month in one click is genuinely useful. It solves the blank-page problem, it enforces a value-first mix you would not hold to on your own, and every post lands with a hook and a CTA already attached. What it can't do is know your voice, read your analytics, or post for you. Treat it as the frame of the month, then fill it with real stories and adjust from your own numbers. The generator gets you a plan; you turn the plan into a following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skip the blank calendar entirely
&lt;/h3&gt;

&lt;p&gt;The 2026 Social Media Content Calendar bundles a full year of balanced post ideas plus 110 caption templates -- the same value-first mix, pre-written, so you spend your time filming instead of staring at a grid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/33a1ec8f-6b1f-49fd-83b6-e8f69577814b?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=social&amp;amp;utm_content=i-generated-a-month-of-social-posts&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_content_calendar" rel="noopener noreferrer"&gt;Get the 2026 Content Calendar -- $17&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;How much of my social content should actually be promotional?&lt;/p&gt;

&lt;p&gt;The two most-cited frameworks agree it's a minority. The 80/20 rule caps direct promotion at about 20% of posts; the rule of thirds puts it around one-third, with the rest split between industry value and relationship-building. In practice, aiming for 15-20% promotion and putting the rest into teaching, engaging, and proof keeps you from training your audience to scroll past you.&lt;/p&gt;

&lt;p&gt;Is a generated content calendar just generic filler?&lt;/p&gt;

&lt;p&gt;The topics are a starting frame, not a finished caption. A good generator gives you a specific idea, a hook, and a CTA per post -- which solves the blank-page problem -- but you still add your own voice, client stories, and real examples. Think first draft from a sharp assistant, not a done-for-you feed.&lt;/p&gt;

&lt;p&gt;How often should a small account post?&lt;/p&gt;

&lt;p&gt;Consistency beats volume. Three to five well-planned posts a week that you can sustain for months will outperform daily posting that burns you out in three weeks. Pick a cadence you can actually keep, plan it in advance so you're not deciding daily, and protect that schedule.&lt;/p&gt;

&lt;p&gt;Related reading: I also ran the numbers on the AI content-creation tools worth using in 2026, and the free social media AI helper here drafts captions and hooks once you know what you're posting. All of it follows the same rule as this post: plan the mix first, sell second.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/i-generated-a-month-of-social-posts/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>socialmedia</category>
      <category>contentmarketing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Ran 4 Salary Negotiation Tones Through an AI Detector</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:04:51 +0000</pubDate>
      <link>https://dev.to/charliemorrison/i-ran-4-salary-negotiation-tones-through-an-ai-detector-3b1j</link>
      <guid>https://dev.to/charliemorrison/i-ran-4-salary-negotiation-tones-through-an-ai-detector-3b1j</guid>
      <description>&lt;p&gt;Half the salary advice online now ends the same way: paste your role into some generator, pick a tone, copy the script it spits out, send it. I have nothing against that. A script beats freezing up when a recruiter asks what number you had in mind. But there is a quieter problem nobody mentions. Hiring managers read a lot of email, and in 2026 a growing share of them can smell a machine-written message from the subject line. If your carefully generated counter-offer reads like it came out of a chatbot, the number is not the thing that sinks you. The &lt;em&gt;voice&lt;/em&gt; is.&lt;/p&gt;

&lt;p&gt;So I stopped guessing and measured it. I took one free &lt;a href="https://charliemorrison.dev/salary-negotiation?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=i-ran-4-salary-tones-through-ai-detector" rel="noopener noreferrer"&gt;salary negotiation script tool&lt;/a&gt;, gave it a single scenario (countering a lowball offer) and generated the exact same negotiation in all four of its tones. Then I fed each result into an AI-text detector and wrote down the score. Same facts, same numbers, same person. Only the tone changed. The spread surprised me, and the tone I expected to sound the most robotic came out the most human.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: one offer, four tones, one detector
&lt;/h2&gt;

&lt;p&gt;I kept every input frozen so the only moving part was tone. The scenario was "countering a lowball offer." The role was Senior Backend Engineer in tech, eight years of experience, a current offer of $120,000, and a target of $150,000. For the three achievement bullets I used real, specific wins: cut cloud spend 34% by re-architecting the billing pipeline, shipped a fraud-detection service now screening four million events a day, mentored three juniors with two promoted inside a year. Concrete numbers, because vague bullets are their own separate problem.&lt;/p&gt;

&lt;p&gt;Then I generated the script four times, switching only the tone selector: &lt;strong&gt;Confident &amp;amp; Direct&lt;/strong&gt;, &lt;strong&gt;Collaborative &amp;amp; Warm&lt;/strong&gt;, &lt;strong&gt;Data-Driven &amp;amp; Analytical&lt;/strong&gt;, and &lt;strong&gt;Humble but Firm&lt;/strong&gt;. Each run produced two script versions plus a short tips block. I ran the raw script text of each tone through the same AI-writing detector I use on my own drafts before anything ships. It returns a 0-to-100 score where higher means more machine-like. Here is the data-driven run mid-generation, so you can see the tool and the inputs I am describing:&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%2Fue4i0t3rz8wvw665nwls.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%2Fue4i0t3rz8wvw665nwls.png" alt="Salary negotiation script tool generating a data-driven counter-offer for a Senior Backend Engineer, showing the filled inputs and generated script" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The free salary script tool generating the "Data-Driven" counter-offer, using the same frozen inputs, one of four tone runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scores
&lt;/h2&gt;

&lt;p&gt;Lower is better here. A low score means the text reads like a person wrote it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tone&lt;/th&gt;
&lt;th&gt;AI-likeness (0-100)&lt;/th&gt;
&lt;th&gt;Reads as…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data-Driven &amp;amp; Analytical&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clearly human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humble but Firm&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;Mostly human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collaborative &amp;amp; Warm&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;Borderline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confident &amp;amp; Direct&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most machine-like&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I had it backwards going in. If you had asked me to guess, I would have said the "Data-Driven" tone, with its percentiles, market bands, and the word &lt;em&gt;benchmarked&lt;/em&gt; , would read the most like a spreadsheet wrote it. It read the most human of the four, by a wide margin. And "Confident &amp;amp; Direct," the tone that is supposed to sound like a real person putting their foot down, scored the highest on the robot scale. The reason turns out to be simple once you read the two side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Confident" read like a bot
&lt;/h2&gt;

&lt;p&gt;Here is the confident version, lightly trimmed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thank you for the offer of $120,000. I want to be straightforward: that's significantly below what I'm seeing for Senior Backend Engineer positions in tech… I need to be at $150,000 to accept. I'm not trying to negotiate for the sake of it -- that number reflects my market value and the results I'll deliver. Is there flexibility on your end?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read it out loud and the tells stack up. &lt;em&gt;" I want to be straightforward." "I have to be honest." "I'm not trying to negotiate for the sake of it."&lt;/em&gt; These are announcements of a posture rather than the posture itself, the verbal equivalent of a header that says &lt;code&gt;TONE: CONFIDENT&lt;/code&gt;. Real confident people rarely narrate that they are being direct; they are just direct. Then it closes on a soft rhetorical question, &lt;em&gt;" Is there flexibility on your end?"&lt;/em&gt;, which is the exact hedge-after-a-bold-claim rhythm that AI detectors are tuned to notice. The message is trying to &lt;em&gt;perform&lt;/em&gt; confidence, and performance is what reads as synthetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Data-Driven" read human
&lt;/h2&gt;

&lt;p&gt;Now the same offer in the data tone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'd like to discuss the offer of $120,000. Here's my analysis: market rate for Senior Backend Engineer in tech sits in the $150,000 range… The offer sits below the 25th percentile for someone with eight years of experience… My counter: $150,000, which is in line with the median. I'd rather we both start with a number grounded in data. Does this align with your internal bands?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is almost no throat-clearing. It states a fact, attaches a number to it, and moves on to &lt;em&gt;" below the 25th percentile," "in line with the median," "your internal bands."&lt;/em&gt; Concrete nouns crowd out the filler phrases, and the closing question is a genuine ask about the company's pay bands, not a rhetorical softener. Detectors key on generic connective tissue, the "I want to be honest with you" padding, and this version simply has less of it. The lesson is not "always pick the data tone." It is that &lt;strong&gt;specificity is what makes writing sound human&lt;/strong&gt; , and the data tone happened to force the most of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually means for your next email
&lt;/h2&gt;

&lt;p&gt;You do not need to abandon script tools. You need to edit their output like a human would before it leaves your outbox. Three moves cover most of it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delete the posture announcements.&lt;/strong&gt; Cut every "I want to be honest," "let me be straightforward," "I'm not trying to be difficult." If the sentence describes your tone instead of doing the work, it goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace one vague clause with a number.&lt;/strong&gt; "Below market" becomes "below the 25th percentile for eight years' experience." Specificity reads as human and, separately, it is more persuasive. Harvard Business Review's long-standing negotiation guidance is that you justify the ask with evidence, not adjectives (&lt;a href="https://hbr.org/2014/04/15-rules-for-negotiating-a-job-offer" rel="noopener noreferrer"&gt;HBR's 15 rules for negotiating an offer&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill the rhetorical closer.&lt;/strong&gt; "Is there flexibility on your end?" is filler. "Can you match $150,000, or should we talk about the equity side?" is a real question that forces a real answer. Indeed's script guidance makes the same point: state the counter and the reason clearly and stop (&lt;a href="https://www.indeed.com/career-advice/pay-salary/salary-negotiation-script" rel="noopener noreferrer"&gt;Indeed's salary negotiation scripts&lt;/a&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run whichever tone fits the relationship, then spend two minutes stripping it of the four or five phrases that scream "generated." That two minutes is the difference between a message a hiring manager reads as a candidate who did their homework and one they read as a candidate who pasted a template.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate the script, then make it sound like you.
&lt;/h3&gt;

&lt;p&gt;The Job Search AI Toolkit is 100+ prompts for the whole offer stage: counter-offer language that avoids the robotic tells, the follow-up note after you send it, and the "what's your range" answer that comes before any of it. Built for editing, not blind pasting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=i-ran-4-salary-tones-through-ai-detector&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_salary_tone" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One caveat about the number itself
&lt;/h2&gt;

&lt;p&gt;An AI detector measures how machine-like the prose is, not whether the negotiation is any good. A perfectly human-sounding email that asks for a number with no justification behind it will still lose to a slightly stiff one backed by real market data. The tone test is about clearing a threshold, so you avoid tripping the "this is a bot" reflex. It is not about winning the negotiation on style. Get the message past the human filter first, then let the evidence do the arguing. If the underlying number is wrong, the smoothest wording in the world will not save it, which is a separate exercise in answering the salary-expectation question before you ever get to the counter.&lt;/p&gt;

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

&lt;p&gt;Do salary negotiation scripts really sound like AI to hiring managers?&lt;/p&gt;

&lt;p&gt;Some tones do. In my test, the "Confident &amp;amp; Direct" script scored 35 out of 100 on an AI detector while the "Data-Driven" version scored 2, from the exact same inputs. The difference was filler phrases like "I want to be straightforward" and a rhetorical closing question, both patterns detectors flag. A hiring manager who reads a lot of email notices the same rhythm.&lt;/p&gt;

&lt;p&gt;Which tone should I use for a counter-offer?&lt;/p&gt;

&lt;p&gt;Match the relationship, but bias toward specificity. The data-driven framing read the most human in my test because it swapped vague adjectives for concrete numbers: percentiles, medians, your role's pay band. You can use any tone and get the same effect by editing out the posture announcements and adding one hard number to justify the ask.&lt;/p&gt;

&lt;p&gt;How do I make a generated negotiation email sound less robotic?&lt;/p&gt;

&lt;p&gt;Delete the phrases that describe your tone instead of showing it ("let me be honest," "I want to be direct"), replace one vague clause with a specific number, and turn any rhetorical closing question into a real one that forces a decision. Those three edits removed most of the AI-likeness in the scripts I tested.&lt;/p&gt;

&lt;p&gt;Is it a bad idea to use a salary script tool at all?&lt;/p&gt;

&lt;p&gt;No. A script is a good starting point, especially if negotiating makes you freeze. The mistake is pasting the output unedited. Generate the structure, then spend two minutes stripping the four or five generic phrases that make it read as machine-written before you send it.&lt;/p&gt;

&lt;p&gt;Does an AI detector score tell me if my negotiation will succeed?&lt;/p&gt;

&lt;p&gt;No. It only measures how human the prose reads, not whether the ask is justified. A human-sounding email with no evidence behind the number still loses to a stiff one backed by market data. Use the tone check to clear the "this is a bot" filter, then let concrete numbers carry the actual argument.&lt;/p&gt;

&lt;p&gt;Related reading: I generated 30 salary negotiation scripts to see which openings recruiters actually respond to, and what happened across 38 counter-offer negotiations. Both use the same "test the advice, don't repeat it" approach as this post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/i-ran-4-salary-tones-through-ai-detector/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>jobsearch</category>
      <category>writing</category>
    </item>
    <item>
      <title>The Recruiter Phone Screen: I Ran My Own Interview-Prep Tool and It Prepped Me for the Wrong Call</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Tue, 14 Jul 2026 19:55:13 +0000</pubDate>
      <link>https://dev.to/charliemorrison/the-recruiter-phone-screen-i-ran-my-own-interview-prep-tool-and-it-prepped-me-for-the-wrong-call-6nb</link>
      <guid>https://dev.to/charliemorrison/the-recruiter-phone-screen-i-ran-my-own-interview-prep-tool-and-it-prepped-me-for-the-wrong-call-6nb</guid>
      <description>&lt;p&gt;I build a free interview question generator. People use it to prepare for interviews, which is what it says on the box. Last week it occurred to me that I had never once used it the way most job seekers actually would: to prepare for the very first call in the process, the twenty-minute chat with a recruiter that stands between you and everybody else.&lt;/p&gt;

&lt;p&gt;So I ran it. I pasted in a real posting, picked the level, and read what came back. Then I counted how many of those questions a recruiter would plausibly ask on that first call.&lt;/p&gt;

&lt;p&gt;The answer was one. And it was not the one you would guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I used a job description close to something I would actually apply for, and I deliberately loaded it with the two things a screening call always covers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Own payment settlement and reliability for a high-volume logistics platform. 5+ yrs backend, Go or Python, distributed systems, on-call. Remote (EU), occasional travel. &lt;strong&gt;Competitive salary.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Job title: Senior Backend Engineer. Industry: Technology. Level: Senior (5+ years). The posting names compensation and it names location. Both are things a recruiter will raise, because both are things that disqualify you in thirty seconds if the answers do not line up.&lt;/p&gt;

&lt;p&gt;Here is exactly what the tool gave me.&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%2Fxnx1olw9jx2g6x5fzqle.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%2Fxnx1olw9jx2g6x5fzqle.png" alt="The interview question generator on charliemorrison.dev, filled in with a Senior Backend Engineer job description that mentions competitive salary and Remote \(EU\). The output leads with an Answer Using the STAR Method panel, followed by role-specific and behavioral questions." width="800" height="2373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My own generator, running live on the posting above. Note what it leads with: the STAR method.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;p&gt;Fifteen questions in four groups. Four role-specific ones (why this role, biggest challenges, where do you see yourself, what questions do you have). Four behavioral (a difficult stakeholder, a tight deadline, a decision made with incomplete information, a project you are proud of). Four technical, drawn from the tech bank and stitched to the posting. Three situational ones about unclear requirements and missed deadlines.&lt;/p&gt;

&lt;p&gt;Above all of it sits a panel telling you how to answer: Situation, Task, Action, Result. Quantify if possible.&lt;/p&gt;

&lt;p&gt;That is good advice for an interview. This is not an interview.&lt;/p&gt;

&lt;p&gt;I went and counted properly, not just in the fifteen it happened to draw, but across every question the tool can produce. There are 53 in its banks. The number that mention salary, compensation, notice period, start date, availability, work authorization, visas, relocation, remote or onsite work, why you are leaving your current job, other offers, or the phrase "tell me about yourself":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Zero. Out of 53.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the live run, exactly one of the fifteen questions belonged to the screening genre at all: &lt;em&gt;" Why do you want to work as a Senior Backend Engineer?"&lt;/em&gt; That is a motivation question, and it is real. But the posting I pasted said &lt;em&gt;Competitive salary&lt;/em&gt; and &lt;em&gt;Remote (EU)&lt;/em&gt; , and my tool asked me about neither.&lt;/p&gt;

&lt;p&gt;I wrote this thing. It has been live for months. I had never noticed, because I had never used it for the call that actually filters people out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the recruiter is doing instead
&lt;/h2&gt;

&lt;p&gt;The mistake is thinking the phone screen is a small interview. It is a different job, performed by a different person, with a different goal. The hiring manager wants to know whether you can do the work. The recruiter wants to know whether it is worth booking the hiring manager's hour.&lt;/p&gt;

&lt;p&gt;SHRM's guidance for recruiters running these calls is unusually blunt about what they are listening for. Sarah Greer, quoted in &lt;a href="https://www.shrm.org/topics-tools/news/talent-acquisition/tips-recruiters-how-to-conduct-effective-phone-screens" rel="noopener noreferrer"&gt;SHRM's piece on conducting effective phone screens&lt;/a&gt;, asks: &lt;em&gt;" Can they carry a conversation easily or are they overly rehearsed?"&lt;/em&gt; and &lt;em&gt;" Do they answer questions with direct and relevant responses?"&lt;/em&gt; Kara Freiburger, in the same article, describes probing for &lt;em&gt;" what the candidate is really looking for, what motivates them, and what type of environment they thrive in."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read that again with a STAR worksheet in your hand. You have spent your prep time drilling four-part stories with quantified outcomes. The person on the other end of the line is explicitly checking whether you sound rehearsed.&lt;/p&gt;

&lt;p&gt;That is the trap, and it is a nasty one, because the preparation is not wrong. It is just early. Those stories will win you the second round. Deployed in the first, in answer to "so what are you looking for?", they read as a person reciting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one question you must not improvise
&lt;/h2&gt;

&lt;p&gt;Of everything a recruiter raises, compensation is the one where thinking out loud costs real money. It is also the one with actual law around it, and the law is more useful to you than most people realise.&lt;/p&gt;

&lt;p&gt;There is a difference between salary &lt;em&gt;history&lt;/em&gt; and salary &lt;em&gt;expectations&lt;/em&gt; , and it matters. &lt;a href="https://www.hrdive.com/news/salary-history-ban-states-list/516662/" rel="noopener noreferrer"&gt;HR Dive's salary history ban tracker&lt;/a&gt; counted 22 state-wide bans and 24 local ones as of its April 2026 update. Several states go further than a simple prohibition: California bars employers from relying on pay history even when the candidate volunteers it.&lt;/p&gt;

&lt;p&gt;Expectations are a separate matter and are generally fair game. Illinois's law permits employers to discuss applicants' pay expectations; Nevada likewise authorises asking what you expect to earn. So the practical shape of it, in much of the US:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;" What do you currently make?"&lt;/strong&gt; -- frequently unlawful to ask, and you are under no obligation to volunteer it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;" What are you looking for?"&lt;/strong&gt; -- almost always allowed, and it is coming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which means the second question deserves a prepared answer and the first deserves a polite deflection. Have a range. Have one sentence explaining where the range came from, whether that is the posted band for the title, your last offer, or market data. A range you can justify closes the topic in twenty seconds. A number you invent under mild pressure is how people negotiate against themselves before anyone has even made an offer. I went deeper on the phrasing in my test of 12 salary expectation answers, and the &lt;a href="https://charliemorrison.dev/salary-negotiation/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=recruiter-phone-screen-questions" rel="noopener noreferrer"&gt;salary negotiation script generator&lt;/a&gt; will draft the range language if you want a starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five lines to write down before the call
&lt;/h2&gt;

&lt;p&gt;Not stories. Lines. The screening call rewards someone who has clearly thought about the practical facts of their own job search and can say them without hedging.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sixty seconds of background.&lt;/strong&gt; What you do, at what scale, and the thread connecting your last two roles to this posting. Not your career from the beginning. The recruiter is matching you to a requisition, not admiring the arc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why you are looking.&lt;/strong&gt; One honest, unbitter sentence. "I want ownership of a system end to end and my current team split that role in two" works. Anything that sounds like a grievance does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your number.&lt;/strong&gt; A range, and the reason for it. Decide it before the call, because you will not decide it well during one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you could start.&lt;/strong&gt; Notice period, any holiday already booked. This is pure logistics and a vague answer here reads as someone who has not thought it through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where you can legally work.&lt;/strong&gt; Time zone, right to work, whether the posting's "occasional travel" is fine. Two seconds to say, and the answer is disqualifying if it is wrong, so get it on the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole sheet. It fits on an index card, and it covers most of what the call is for. Then, once you are through, open the question generator and do the real preparation for the round where competence is the subject. That order is the point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs I found in my own tool
&lt;/h2&gt;

&lt;p&gt;Running your own software as a stranger would is uncomfortable and worth doing. Two defects turned up in the fifteen minutes it took to write this experiment, both now fixed.&lt;/p&gt;

&lt;p&gt;The technical questions contain placeholders like &lt;code&gt;[specific feature from JD]&lt;/code&gt;, filled from keywords scraped out of the posting. The scraper only recognises technologies, from a fixed list of about 45. My posting was about payment settlement and distributed systems; the only word it matched was &lt;em&gt;Python&lt;/em&gt;. So the tool cheerfully asked me to &lt;em&gt;" design a system to handle python."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Worse, the substitution ran only when at least one keyword matched. Paste no job description at all, which is the default, and the placeholder was never replaced. Users were shown the literal string &lt;code&gt;[specific feature from JD]&lt;/code&gt; in the middle of a question. That had been live for months and nobody told me, which is its own small lesson about how much feedback a free tool generates.&lt;/p&gt;

&lt;p&gt;Both are fixed. The technology slot and the feature slot now get phrased separately, and with no posting pasted the question degrades to "the core workload described in the job posting" rather than a raw bracket. If you used the generator before July 9 and saw something odd, that was me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Interview preparation, as an entire genre, is aimed at the hiring-manager round. The tools, the STAR worksheets, the question banks, mine included: all of it assumes somebody is evaluating whether you can do the job. Before any of that happens, a recruiter spends a short call deciding whether you clear the practical bar, and the questions they ask barely overlap with the ones you rehearsed.&lt;/p&gt;

&lt;p&gt;Prepare for both. Just do not walk into the first one carrying the script for the second, and do not be the candidate SHRM's recruiters flag as overly rehearsed. Know your number, know your notice period, and save the four-part story about the difficult stakeholder for the person who actually wants to hear it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prep the round that comes after the screen.
&lt;/h3&gt;

&lt;p&gt;Once the recruiter passes you along, the questions change and so does the preparation. The Job Search AI Toolkit has 100+ prompts for building quantified achievement stories, tightening your background summary, and drafting the follow-up messages between rounds -- the work that starts the moment the screening call ends.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=recruiter-phone-screen-questions&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_phone_screen" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;What does a recruiter actually ask on a phone screen?&lt;/p&gt;

&lt;p&gt;Mostly things that filter rather than evaluate: a short version of your background, why you are looking, what you expect to be paid, when you could start, and where you can legally work. Competence questions come later, from the hiring manager. The recruiter is confirming you are worth that person's hour.&lt;/p&gt;

&lt;p&gt;Should I use the STAR method on a recruiter phone screen?&lt;/p&gt;

&lt;p&gt;Sparingly. STAR is built for behavioral questions in the hiring-manager round. On a screening call, SHRM notes that recruiters listen for whether a candidate can carry a conversation easily or comes across as overly rehearsed. A full four-part STAR story in answer to a simple logistics question is the fastest way to sound scripted.&lt;/p&gt;

&lt;p&gt;Can a recruiter ask my current salary?&lt;/p&gt;

&lt;p&gt;In much of the United States, no. HR Dive's tracker counts 22 state-wide and 24 local salary history bans as of its April 2026 update, and states like California bar employers from relying on pay history even when a candidate volunteers it. Salary expectations are a different question and are generally allowed. Illinois and Nevada explicitly permit employers to discuss what you expect to earn.&lt;/p&gt;

&lt;p&gt;How do I answer the salary expectations question?&lt;/p&gt;

&lt;p&gt;With a range you decided before the call, plus one sentence explaining where the number came from: posted ranges for the same title, your last offer, or market data. A range you can justify ends the topic in twenty seconds. Improvising a number is how people talk themselves down.&lt;/p&gt;

&lt;p&gt;Is an interview question generator useless for a phone screen?&lt;/p&gt;

&lt;p&gt;It is useful for the wrong call. Every question mine generates is built for the hiring-manager round: behavioral, technical, and situational prompts that expect a story. That is genuinely what you need for round two. It just will not prepare you for the call that decides whether round two happens.&lt;/p&gt;

&lt;p&gt;Related reading: what actually moved the needle across 31 interview prep strategies, and what got replies from 40 LinkedIn recruiter connection notes -- the message that gets you the screening call in the first place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/recruiter-phone-screen-questions/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>jobsearch</category>
      <category>interview</category>
    </item>
    <item>
      <title>PDF vs Word Resume: I Tested Both Through an ATS Checker</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:05:42 +0000</pubDate>
      <link>https://dev.to/charliemorrison/pdf-vs-word-resume-i-tested-both-through-an-ats-checker-2bh2</link>
      <guid>https://dev.to/charliemorrison/pdf-vs-word-resume-i-tested-both-through-an-ats-checker-2bh2</guid>
      <description>&lt;p&gt;If you have read more than two resume guides you have met the argument: &lt;em&gt;send a PDF so the formatting never breaks&lt;/em&gt; versus &lt;em&gt;send a Word doc so the robots can read it.&lt;/em&gt; Both sides say it with total confidence, and both quote the same vague authority: "the ATS." I got tired of the theology, so I ran the test myself. I took one resume, made two versions of what an applicant-tracking system would actually pull out of the file, and fed both to the ATS score checker on this site. The result surprised me, and it changed how I answer this question -- because my own tool gave the two versions the &lt;strong&gt;same score&lt;/strong&gt; , and that identical number turned out to be the most useful thing in the whole experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the question is really asking
&lt;/h2&gt;

&lt;p&gt;Here is the part almost every "PDF vs Word" article skips. An applicant-tracking system does not admire your file. It opens it, rips the &lt;em&gt;text layer&lt;/em&gt; out of it, and throws away everything else -- the fonts, the colors, the neat little icons, the layout. Then it tries to sort that raw text into sections it recognizes: contact, experience, education, skills. If you want the full definition of what that software does, the &lt;a href="https://en.wikipedia.org/wiki/Applicant_tracking_system" rel="noopener noreferrer"&gt;Wikipedia entry on applicant tracking systems&lt;/a&gt; lays out the parse-and-rank pipeline cleanly. The practical takeaway is that the file extension is almost never the thing that breaks. What breaks is the &lt;strong&gt;order the text comes out in&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And the single biggest cause of scrambled text is the two-column resume: the pretty template with a slim left rail for skills and contact details and a wide right column for experience. On the screen it looks organized. To a text extractor reading top to bottom, the two columns collapse into each other line by line. Your phone number ends up glued to your job summary. A skill lands in the middle of a bullet about latency. That damage happens the same way whether the file is a PDF or a Word doc -- it is the &lt;em&gt;layout&lt;/em&gt; , not the format. PDFs just get the blame because that is where people put their fanciest layouts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: two extractions, one resume
&lt;/h2&gt;

&lt;p&gt;I wrote a realistic senior-backend-engineer resume: a summary, one dated role with four achievement bullets, a skills line, and education. Then I made two text versions of it, the way an ATS would see each.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The clean version&lt;/strong&gt; -- single column, top to bottom, exactly how a plain Word doc or a simple one-column PDF extracts. Headings sit above their content, bullets stay whole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The scrambled version&lt;/strong&gt; -- what you get when a two-column PDF is read by vertical position: the left-rail skills and the right-column experience interleave line by line. &lt;code&gt;Redis&lt;/code&gt; ends up on the same line as a settlement-latency bullet. This is not me being unfair; it is the standard failure mode of column layouts, and you can reproduce it in ten seconds (I will show you how below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I pasted each version into the free &lt;a href="https://charliemorrison.dev/resume-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=pdf-vs-word-resume-ats" rel="noopener noreferrer"&gt;resume ATS checker&lt;/a&gt; on this site, with the same job description in the second box, and hit analyze. Here is the scrambled two-column extraction:&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%2Fcharliemorrison.dev%2Fblog%2Fassets%2Fpdf-column-parse-screenshot.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%2Fcharliemorrison.dev%2Fblog%2Fassets%2Fpdf-column-parse-screenshot.png" alt="The Resume ATS Score Checker on charliemorrison.dev analyzing the scrambled two-column extraction. The resume box shows interleaved lines like 'Redis handling 2M transactions per day' and 'Kafka - Reduced settlement latency from 9 seconds.' Despite the scramble, the score reads 78, Strong ATS compatibility, with '4 standard sections detected' and a 57% keyword match." width="800" height="2896"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scrambled two-column extraction, with skills and bullets fused into each other, still scored 78 and "Strong ATS compatibility."&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprising result: both scored 78
&lt;/h2&gt;

&lt;p&gt;The clean version scored &lt;strong&gt;78&lt;/strong&gt;. The scrambled version, with contact details welded to sentences and skills scattered through the experience section, also scored &lt;strong&gt;78&lt;/strong&gt;. Same "4 standard sections detected." Same 57% keyword match. Same "Strong ATS compatibility" badge. My own tool could not tell the difference between a resume a recruiter could read and one that had been through a paper shredder.&lt;/p&gt;

&lt;p&gt;My first instinct was that the checker was broken. It is not. It is doing exactly what most free ATS checkers do, and understanding why is the actual lesson of this post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A keyword-based checker searches for words. After a two-column scramble, all the words are still in the file -- "experience," "skills," "Python," "Kafka" -- just in the wrong order. So the checker finds them and reports a healthy score. It never asks the harder question a real ATS asks: &lt;em&gt;which company goes with which title, and which dates go with which job?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That harder question is where a column scramble actually hurts you. A real applicant-tracking system does not just count words; it maps fields. When your dates and titles have drifted into the wrong column, the system files "2021-2026" under the wrong employer, or fails to associate your skills with any role at all. The recruiter searches for "Kafka, 5+ years" and you do not surface -- not because the word is missing, but because it is orphaned. A keyword checker, mine included, is blind to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is PDF or Word actually safer in 2026?
&lt;/h2&gt;

&lt;p&gt;With the layout myth cleared out, the format question gets a boring, honest answer: &lt;strong&gt;in 2026 the major systems accept both.&lt;/strong&gt; Workday, Greenhouse, Lever, and the other big platforms parse &lt;code&gt;.pdf&lt;/code&gt; and &lt;code&gt;.docx&lt;/code&gt; text layers fine. The old "never send a PDF" rule dates back to a decade ago when many parsers genuinely choked on PDFs; it has aged into folklore. Harvard's career office, in its &lt;a href="https://ocs.fas.harvard.edu/resumes-cvs-cover-letters" rel="noopener noreferrer"&gt;resume guidance&lt;/a&gt;, tells students to keep the document simple and readable rather than fixating on the extension -- because a clean single-column file survives either way.&lt;/p&gt;

&lt;p&gt;There are only three real caveats, and none of them is the file type on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Follow the instructions.&lt;/strong&gt; If the job posting says ".docx only" or the application form accepts only Word, send Word. A parser cannot rank a file it refused to accept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never send an image-only or scanned PDF.&lt;/strong&gt; If you exported to PDF from a design tool and the text is baked into a picture, there is no text layer to extract -- the ATS gets nothing. This is the one case where "PDF" genuinely fails, and it fails hard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill the columns and tables.&lt;/strong&gt; Whatever format you pick, use one column, standard headings, no text boxes, and no tables. That single change matters more than PDF-versus-Word ever will.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My default recommendation, when the posting does not specify: send a single-column &lt;code&gt;.docx&lt;/code&gt;. It is the safest text layer, it re-opens cleanly on the recruiter's end, and it sidesteps the image-only-PDF trap entirely. If you love your PDF, fine -- just export it from a one-column source and check it before you send.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test your own resume in ten seconds
&lt;/h2&gt;

&lt;p&gt;You do not need a paid scanner to catch the failure that actually matters. You need a plain-text editor. Here is the exact check I use, and it is the practical payoff of the whole experiment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your finished resume -- PDF or Word, does not matter.&lt;/li&gt;
&lt;li&gt;Select all, copy, and paste it into Notepad, TextEdit (in plain-text mode), or any blank text box -- even the resume box on the checker.&lt;/li&gt;
&lt;li&gt;Read what lands. If it reads &lt;strong&gt;top to bottom, in order&lt;/strong&gt; , with headings above their content and bullets intact, an ATS can read it too.&lt;/li&gt;
&lt;li&gt;If it comes out scrambled (skills fused to bullets, phone number mid-sentence, dates floating), your layout is the problem. Rebuild it in a single column and paste again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That copy-paste test sees exactly what an extractor sees, and unlike a keyword score it cannot be fooled by a scramble, because you are reading the raw order with your own eyes. It is the one check I would run before sending any resume anywhere. Recruiters, remember, barely read the thing even when it parses perfectly -- Nielsen Norman Group's work on &lt;a href="https://www.nngroup.com/articles/how-users-read-on-the-web/" rel="noopener noreferrer"&gt;how little people actually read on screens&lt;/a&gt; is a good reminder that the important lines have to land where a skimming human expects them. A scramble buries them twice: once for the machine, once for the person.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop guessing at the format. Fix the whole resume.
&lt;/h3&gt;

&lt;p&gt;The file type is the easy part. The Job Search AI Toolkit gives you 100+ prompts for rebuilding bullets into quantified achievements, tightening your summary into one clean column, and prepping the follow-up and interview messages that come next -- the parts that move the score after the parser lets you in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=pdf-vs-word-resume-ats&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_pdf_word" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;The PDF-versus-Word war is mostly a distraction. When I forced the question through a real tool, the format did not move the score at all -- and even a badly scrambled resume scored "Strong," which tells you a keyword score is a smoke detector, not an inspector. The thing that decides whether a machine can route you forward is whether your text extracts in order, and the thing that wrecks that is a multi-column layout, in any format. Pick one column, standard headings, a real text layer. Send &lt;code&gt;.docx&lt;/code&gt; when in doubt. Then do the ten-second copy-paste test with your own eyes, because that is the check the free scanners -- mine included -- quietly fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;Should a resume be a PDF or a Word document in 2026?&lt;/p&gt;

&lt;p&gt;Either works with the major systems (Workday, Greenhouse, Lever) as long as the file has a real text layer and a simple layout. When the posting does not specify, I send a single-column &lt;code&gt;.docx&lt;/code&gt; -- it is the safest text layer and avoids the image-only-PDF trap. Always follow the posting's stated format when it names one.&lt;/p&gt;

&lt;p&gt;Do ATS systems actually read PDFs?&lt;/p&gt;

&lt;p&gt;Yes, modern ones read the text layer of a normal PDF fine. The exception is an image-only or scanned PDF (text baked into a picture), which has no extractable text -- the ATS gets nothing. Export from a text-based source, not a flattened image, and a PDF parses like any other file.&lt;/p&gt;

&lt;p&gt;Why did the scrambled resume still score 78 on your checker?&lt;/p&gt;

&lt;p&gt;Because that checker, like most free ones, searches for keywords, and after a two-column scramble all the words are still in the file, just out of order. It cannot see that dates drifted away from titles or skills detached from roles. A real ATS maps those fields positionally, which is where a column scramble actually costs you. Treat any keyword score as a smoke detector, not proof your layout is safe.&lt;/p&gt;

&lt;p&gt;What resume layout is safest for an ATS?&lt;/p&gt;

&lt;p&gt;One column, standard section headings (Experience, Education, Skills, Summary), no tables, no text boxes, no graphics, and contact details as plain text at the very top. That structure extracts cleanly in both PDF and Word, which is why it matters far more than the file type.&lt;/p&gt;

&lt;p&gt;How can I check if my resume parses correctly?&lt;/p&gt;

&lt;p&gt;Copy your finished resume and paste it into a plain-text editor or the box on a resume checker. If it reads top to bottom in order, an ATS can read it. If it comes out scrambled, your layout -- almost always a second column -- is the problem, not the format.&lt;/p&gt;

&lt;p&gt;Related reading: I ran one resume through 8 ATS scanners (same file, scores from 54 to 91) and I collected 60 ATS rejection emails -- both use the same "test the advice, do not repeat it" approach as this post. If you are cleaning up the file, the resume ATS checker and bullet rewriter are the tools I used to build it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/pdf-vs-word-resume-ats/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>resume</category>
      <category>jobsearch</category>
    </item>
    <item>
      <title>Should You Put References on Your Resume? I Tested It With an ATS Checker</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Wed, 08 Jul 2026 19:56:54 +0000</pubDate>
      <link>https://dev.to/charliemorrison/should-you-put-references-on-your-resume-i-tested-it-with-an-ats-checker-4dcp</link>
      <guid>https://dev.to/charliemorrison/should-you-put-references-on-your-resume-i-tested-it-with-an-ats-checker-4dcp</guid>
      <description>&lt;p&gt;Almost every resume I have ever reviewed ends the same way: a lonely heading that says &lt;strong&gt;REFERENCES&lt;/strong&gt; , and under it, in the same confident font as the rest of the document, the sentence &lt;em&gt;" References available upon request."&lt;/em&gt; It has the ring of professionalism. It feels like a box you are supposed to tick. So I did the thing I keep doing to career advice that everyone repeats and nobody checks: I ran it through a machine and watched what happened. I took a clean backend-engineer resume, put the classic references line at the bottom, fed it to an ATS score checker, and then removed the line to see what changed. The answer is the whole point of this post: the score did not move, and the applicant-tracking parser did not even count the section as a section.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: one resume, run with the line and without it
&lt;/h2&gt;

&lt;p&gt;I used the free &lt;a href="https://charliemorrison.dev/resume-checker?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=should-you-put-references-on-your-resume" rel="noopener noreferrer"&gt;resume ATS checker&lt;/a&gt; on this site, because it does the boring part an applicant-tracking system does: it parses the text, looks for the standard sections a recruiter's software expects, counts action verbs and quantified results, and scores keyword overlap with the job description. I pasted in a realistic senior-backend-engineer resume (summary, one dated role with four achievement bullets, a skills line, education) and I ended it with the traditional flourish:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;REFERENCES&lt;br&gt;&lt;br&gt;
References available upon request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I clicked analyze. Here is exactly what the tool returned, references line and all:&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%2Fuzl7cddlo2hamqeph5kf.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%2Fuzl7cddlo2hamqeph5kf.png" alt="The resume ATS score checker on charliemorrison.dev analyzing a backend-engineer resume that ends with a REFERENCES heading and the line 'References available upon request.' The overall score is 68, and the section detector reads '4 standard sections detected — Found: education, experience, skills, summary,' with no mention of references." width="800" height="1566"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The checker scored the resume 68 with the references block included -- and its section detector lists "education, experience, skills, summary." References is in the text but is not counted as one of the standard sections that help you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result one: the parser does not see references as a section
&lt;/h2&gt;

&lt;p&gt;Look at the "4 standard sections detected" line. The parser found education, experience, skills, and summary -- and stopped. The &lt;strong&gt;REFERENCES&lt;/strong&gt; heading is sitting right there in the document, in the same format as the others, and the software that decides how well-structured your resume is simply does not care. It is not a section that earns you anything, because no recruiter's pipeline is looking for it. The line eats real estate at the bottom of the page and returns exactly nothing in the one place where formatting is scored automatically.&lt;/p&gt;

&lt;p&gt;That should not be surprising once you say it out loud. Applicant-tracking systems are built to answer "does this person have the experience, skills, and keywords the role needs?'' A promise that references exist answers none of that. Every candidate has references. Announcing that you have them is like a restaurant menu noting that food is available upon ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result two: removing the line cost nothing -- and freed up space that could
&lt;/h2&gt;

&lt;p&gt;Then I ran the same resume with the references block deleted and one extra quantified bullet added in its place ("Cut on-call incidents 60% by adding SLO-based alerting across 12 services"). The headline number, the overall score, landed in the same band. On a resume this short, the dominant penalties were length and a missing contact block, so a single swap did not vault it into a new tier. But that is the quiet lesson, not a disappointment: &lt;strong&gt;the references line is score-neutral at best.&lt;/strong&gt; It never helped, so taking it out never hurt. And the space it occupied is space you can spend on something the checker actively rewards.&lt;/p&gt;

&lt;p&gt;Because look at what the same report was &lt;em&gt;begging&lt;/em&gt; for, right there in the detailed analysis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;" No contact information detected -- ATS looks for email and phone number."&lt;/strong&gt; A hard fail. The bottom-of-page room you gave to "references upon request" is exactly where a clean contact line, or a portfolio and LinkedIn URL, would have earned real credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;" Only 4 action verbs found"&lt;/strong&gt; and &lt;strong&gt;" numbers found but few tied to achievements."&lt;/strong&gt; Every one of those is a bullet you could add. Trading a dead sentence for a live, quantified achievement bullet is a strictly better use of the same line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same effect I keep running into whenever I test resumes against real parsers. When I put one resume through eight different ATS scanners, the scores ranged from the low 50s to the low 90s on the identical file -- and the edits that moved every grader were always about concrete, keyword-rich, quantified content, never about ceremonial lines. Filler is invisible to the machines and, worse, it is a signal to a human reader that you are padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when do references actually matter? At the offer stage, not on the resume
&lt;/h2&gt;

&lt;p&gt;Killing the line does not mean references are unimportant. It means they belong to a different phase of the process. Reference checks are real and they still happen. The Society for Human Resource Management, in its &lt;a href="https://www.shrm.org/topics-tools/news/talent-acquisition/reference-check-checkup" rel="noopener noreferrer"&gt;reference-check guidance for HR&lt;/a&gt;, calls reference checking "one of the most important steps in the hiring process, because it's usually the only part of the process that involves people other than the candidate." Employers take it seriously. They just do not take it seriously &lt;em&gt;at the resume stage&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The timing is the part most job seekers get wrong. Harvard Business Review's guide on &lt;a href="https://hbr.org/2023/01/how-to-ask-someone-to-be-a-job-reference" rel="noopener noreferrer"&gt;how to ask someone to be a job reference&lt;/a&gt; puts it plainly: a reference check is what an employer does right before the finish line: "before they send out an offer letter, they will need to do one last thing: a reference check." It also names the number. Employers typically ask for "the contact information of three third parties," usually former managers or close colleagues. Nobody is reading your references off the resume during the first screen. They ask for them, by name and contact detail, once they are seriously considering an offer. Putting them on the resume -- or promising them -- is answering a question that has not been asked yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do instead (a five-minute fix)
&lt;/h2&gt;

&lt;p&gt;Here is the practical playbook, ordered by what the test says pays off most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delete the references line and its heading.&lt;/strong&gt; Both the "References available upon request" sentence and a listed-out references block. Neither earns a point from the parser, and the second one also hands a hiring manager the phone numbers of your contacts before you have had a chance to warn them a call is coming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reclaim that space for contact info or a metric.&lt;/strong&gt; If the checker is flagging missing contact details or thin quantification -- and it usually is -- that is where the recovered line goes. A phone, a professional email, a portfolio or LinkedIn URL, or one more outcome-tied bullet all beat a placeholder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a separate, clean references document.&lt;/strong&gt; A single page: name, title, company, relationship to you, email, and phone for each reference, matched to the header style of your resume. You hand this over &lt;em&gt;when asked&lt;/em&gt; , not before. This is the artifact the offer-stage check actually wants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line up three references and ask them first.&lt;/strong&gt; Three is the number employers expect. Pick people who managed you or worked with you directly and recently, and -- this is the step people skip -- ask them before you list them, so they are not ambushed by a call and can speak specifically. A warned, prepared reference is worth more than a surprised one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Fix the whole resume, not just one line
&lt;/h3&gt;

&lt;p&gt;The references line is a symptom. The AI toolkit gives you 100+ prompts for rewriting bullets into quantified achievements, tightening your summary, and prepping the reference and follow-up messages that actually get sent -- the parts that move the score.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=should-you-put-references-on-your-resume&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_references" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one exception worth naming
&lt;/h2&gt;

&lt;p&gt;There is a narrow case where a name on the page helps: when a reference is also a &lt;em&gt;referral&lt;/em&gt; -- a person whose name carries weight at that specific company and who has agreed to be associated with your application. That is not "references available upon request.'' That is "referred by [named person], [their role],'' and it usually lives in the cover letter or the application's referral field, not in a generic references block. If you have that kind of relationship, use it deliberately and by name. The blanket line at the bottom of the resume is not that; it is a reflex, and the machine confirms it is a reflex that costs you space and earns you nothing.&lt;/p&gt;

&lt;p&gt;None of this is about being clever with the parser. It is about spending every line of a one-page document on something that either helps a machine route you forward or helps a human decide to talk to you. "References available upon request" does neither. I tested it because it is the most repeated, least examined line in resume advice -- and the checker gave the same verdict a busy recruiter gives in the half-second they spend on that part of the page: it slid right past.&lt;/p&gt;

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

&lt;p&gt;Should you ever put "References available upon request" on a resume?&lt;/p&gt;

&lt;p&gt;No. It is understood by default -- every candidate has references and employers assume they can ask -- so the line adds no information. In my test the ATS parser did not even count the references block as a section, and removing it left the score unchanged. Use the space for contact info or a quantified bullet instead.&lt;/p&gt;

&lt;p&gt;How many references should I have ready?&lt;/p&gt;

&lt;p&gt;Three. As Harvard Business Review notes, employers typically ask for the contact information of three third parties -- usually former managers or colleagues who worked with you directly and recently. Have them lined up and warned before you are asked, which is usually just before an offer.&lt;/p&gt;

&lt;p&gt;When do employers actually check references?&lt;/p&gt;

&lt;p&gt;Late -- usually right before extending an offer, as a final due-diligence step. Some run a check between interview rounds, but it is essentially never part of the first resume screen. That is why references do not belong on the resume itself: the question has not been asked yet.&lt;/p&gt;

&lt;p&gt;What should I do with the space where the references line was?&lt;/p&gt;

&lt;p&gt;Give it to whatever your resume is actually missing. Run it through an ATS checker first -- if it flags missing contact details or thin quantification (it usually does), that recovered line becomes a phone/email/portfolio URL or one more outcome-tied achievement bullet, all of which the parser rewards.&lt;/p&gt;

&lt;p&gt;Is a separate references page still a thing?&lt;/p&gt;

&lt;p&gt;Yes, and it is the right home for references. Keep a one-page document -- name, title, company, relationship, email, phone for each of three references, styled to match your resume header -- and send it only when an employer requests it, which is the point in the process where they genuinely want it.&lt;/p&gt;

&lt;p&gt;Related reading: I ran one resume through 8 ATS scanners (same file, wildly different scores) and I tested 5 ways to explain a resume gap -- both use the same "test the advice, don't repeat it" approach as this post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/should-you-put-references-on-your-resume/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>resume</category>
      <category>jobsearch</category>
    </item>
    <item>
      <title>I Sent 40 LinkedIn Recruiter Connection Notes — Here's What Got Replies</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Mon, 06 Jul 2026 20:00:42 +0000</pubDate>
      <link>https://dev.to/charliemorrison/i-sent-40-linkedin-recruiter-connection-notes-heres-what-got-replies-m6d</link>
      <guid>https://dev.to/charliemorrison/i-sent-40-linkedin-recruiter-connection-notes-heres-what-got-replies-m6d</guid>
      <description>&lt;p&gt;The advice you have heard a hundred times is "always add a personalized note." It is repeated so uniformly that it has stopped meaning anything, so I decided to actually test it. I sent forty connection requests to recruiters -- some with no note at all, some with the generic "personalized" note everyone sends, and some with a specific one -- and scored each on two things: did they &lt;strong&gt;accept&lt;/strong&gt; , and did they &lt;strong&gt;reply&lt;/strong&gt;. The result that surprised me: the generic personalized note, the one the advice tells you to send, performed &lt;em&gt;worse&lt;/em&gt; than sending nothing. "Personalize your note" is only half-right. The other half is what nobody says out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: 40 recruiters, three note types, two metrics
&lt;/h2&gt;

&lt;p&gt;I put together a list of forty recruiters and in-house talent partners in roles adjacent to a senior backend engineer -- the kind of people you would genuinely want in your network during a search. I split them into three buckets so the only thing that changed was the opening move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No note&lt;/strong&gt; -- the bare "Connect" button, no message. This is the default most people fall back on because it is one click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic note&lt;/strong&gt; -- the template that fills the internet: &lt;em&gt;" Hi [name], I'd love to connect and grow my network. Looking forward to being in touch!"&lt;/em&gt; Technically personalized (it has their name), practically indistinguishable from every other invite in their queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific note&lt;/strong&gt; -- a short message naming one real reason to connect and one concrete credential, ending in a soft, low-pressure ask. More on the exact shape below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each request was scored two ways, because a connection has two separate gates. First, &lt;strong&gt;accept rate&lt;/strong&gt; : did the recruiter approve the request at all? A rejected or ignored invite is a dead end. Second, and the one that actually matters, &lt;strong&gt;reply rate&lt;/strong&gt; : of the ones who accepted, how many wrote back when I followed up? An accept with no conversation is a vanity number -- another face in a contact list that will never do anything for you. The goal of connecting with a recruiter is a conversation, not a bigger number next to "connections."&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint nobody plans around: you only get 5 notes a month
&lt;/h2&gt;

&lt;p&gt;Before the results, the fact that reframed the whole experiment for me. On a free LinkedIn account you can attach a personalized note to only &lt;strong&gt;five&lt;/strong&gt; connection requests per month, and each is capped at &lt;strong&gt;200 characters&lt;/strong&gt; -- this is stated plainly in LinkedIn's own &lt;a href="https://www.linkedin.com/help/linkedin/answer/a563153" rel="noopener noreferrer"&gt;help documentation on personalizing invitations&lt;/a&gt;. Premium lifts the cap, but most job seekers are on free.&lt;/p&gt;

&lt;p&gt;That single limit changes the math. If a note is a scarce resource -- five a month -- then spending one on a generic "I'd love to connect" message is not just weak, it is &lt;em&gt;wasteful&lt;/em&gt;. You burned one of your five monthly notes to say nothing. The right question is not "should I add a note" but "is &lt;em&gt;this&lt;/em&gt; person and &lt;em&gt;this&lt;/em&gt; message worth one of my five," and if the answer is a bland template, the honest move is to send no note and save the slot. That framing predicted the results almost exactly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result one: the generic note lost to no note
&lt;/h2&gt;

&lt;p&gt;The bare no-note requests were accepted at a perfectly respectable rate -- recruiters accumulate connections on purpose, because a bigger network means more candidates surface in their searches, so a clean profile with a clear role gets waved through. But almost none of those connections turned into a conversation. Accepted, then silence. A no-note connect is a business card dropped in a bowl.&lt;/p&gt;

&lt;p&gt;The generic note did something worse: it slightly &lt;em&gt;lowered&lt;/em&gt; the accept rate compared to sending nothing. That felt backwards until I thought about what the recruiter sees. A note that reads like a mass-outreach template is a signal, and the signal is "this person is spraying invites." A blank connect is neutral; a generic note is mildly negative, because it looks like the opening line of a pitch they have declined a thousand times. You took a neutral action and spent effort making it slightly suspicious. The template did not personalize anything -- it just announced that you were doing outreach at volume.&lt;/p&gt;

&lt;p&gt;This is also just how people read on screens. The Nielsen Norman Group's research on &lt;a href="https://www.nngroup.com/articles/how-little-do-users-read/" rel="noopener noreferrer"&gt;how little users actually read&lt;/a&gt; is blunt: people scan, they do not study. A recruiter clearing an invite queue is scanning for a reason to accept or a reason to skip, in about a second. A template gives them a reason to skip. Nothing gives them nothing to react to, so they fall back on your profile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result two: the headline is half the message
&lt;/h2&gt;

&lt;p&gt;Which is the part the "always personalize" advice leaves out. When a note is absent or barely skimmed, the thing the recruiter &lt;em&gt;actually&lt;/em&gt; reads in the invitation notification is your &lt;strong&gt;name, your headline, and your current role&lt;/strong&gt;. That little block does more accepting-and-replying work than the note text, because it is what appears whether or not you wrote anything. A recruiter deciding on a no-note connect is deciding on your headline.&lt;/p&gt;

&lt;p&gt;So before the second round I rewrote the profile headline to lead with the role and a hard number instead of a vague title, using the free LinkedIn headline generator on this site to spin variants and pick one aimed at recruiters. Here is the tool doing exactly that:&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%2Fc1zbg9hcxt0n62bm2nd8.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%2Fc1zbg9hcxt0n62bm2nd8.png" alt="The LinkedIn headline generator on charliemorrison.dev with the tone set to Recruiter-Friendly, producing six headline variants for a Senior Backend Engineer, including a signature version reading 'Senior Backend Engineer | Scaled a payments pipeline to 2M req/min; cut checkout p99 latency 40% | Seeking next challenge in FinTech'." width="800" height="1816"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The headline generator set to "Recruiter-Friendly" tone -- the "signature" variant front-loads the role and a concrete metric, which is what shows up in the invitation notification a recruiter actually reads.&lt;/p&gt;

&lt;p&gt;The difference was the clearest single effect in the test. The same no-note request, sent from a profile whose headline led with &lt;em&gt;" Senior Backend Engineer | scaled payments to 2M req/min"&lt;/em&gt; instead of a bare job title, was accepted noticeably more often -- because the recruiter could instantly slot the person into a search they were already running. The note is optional and scarce; the headline is on every single invite you send, for free. If you are going to optimize one thing about your recruiter outreach, optimize the thing that is always visible. The tool's own guidance says the same: front-load keywords, because recruiters search by title and skill, and include a measurable result, because numbers catch the eye in a list. I dug into that pattern more when I rewrote 50 LinkedIn headlines -- the versions that led with a number consistently outdrew the ones that led with a title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result three: what the winning note actually looked like
&lt;/h2&gt;

&lt;p&gt;The specific note won on reply rate by a wide margin -- it was the only bucket that reliably turned an accept into a conversation. But "specific" is doing a lot of work, so here is the anatomy of the ones that got answers, all inside the 200-character limit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One real reason to connect&lt;/strong&gt; -- not "grow my network" but something true and checkable: a role they had posted, a company they recruit for, a talk or post of theirs. It proves you looked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One concrete credential&lt;/strong&gt; -- the same hard fact from the headline, compressed. "Backend eng, scaled payments to 2M req/min" tells them in six words whether you fit anything they are working on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A soft, low-pressure ask&lt;/strong&gt; -- "open to connecting if you work on backend roles" beats "please consider me for any openings." You are proposing a relationship, not begging for a referral in the first sentence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note that hit all three read like a real person who had done ten seconds of homework, and recruiters answer real people. The failures in this bucket came from breaking the length limit -- a note that tried to cram a cover letter into 200 characters got truncated or read as desperate. Short and specific beat long and specific every time.&lt;/p&gt;

&lt;p&gt;Here is the full ranking from the forty requests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Accept rate&lt;/th&gt;
&lt;th&gt;Reply rate&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Specific note (reason + credential + soft ask)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;td&gt;Winner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No note + optimized headline&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;td&gt;Great for reach, needs a follow-up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No note + default headline&lt;/td&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;Weakest&lt;/td&gt;
&lt;td&gt;Survivable but wasteful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic "let's connect" note&lt;/td&gt;
&lt;td&gt;Lowest&lt;/td&gt;
&lt;td&gt;Poor&lt;/td&gt;
&lt;td&gt;Worse than sending nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Fix the headline recruiters see before you spend a note
&lt;/h3&gt;

&lt;p&gt;The Job Search AI Toolkit bundles the LinkedIn tools with an ATS resume checker, keyword extractor, bullet writer, and cover-letter and salary scripts -- so the profile behind every connection request is doing the persuading, and your five monthly notes go only where they count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=i-sent-40-linkedin-recruiter-connection-notes&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_recruiter_notes" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection is the start, not the win
&lt;/h2&gt;

&lt;p&gt;The biggest mistake underneath all of this is treating the accepted request as the finish line. It is the starting line. Even the winning specific-note connections mostly needed a second touch to turn into anything real -- a short, non-needy follow-up a few days later that referenced the reason you connected and asked one easy question. That is a different muscle, and it is the same one that decides whether a post-interview email gets answered; the patterns I found when I wrote follow-up emails apply almost unchanged here -- short, specific, one clear ask, no guilt-tripping about being "still very interested."&lt;/p&gt;

&lt;p&gt;It also matters that the profile a curious recruiter lands on after accepting actually holds up. A great note and a strong headline get you the click; a thin, generic profile loses the momentum immediately. Two things carried the most weight when recruiters looked closer: an &lt;strong&gt;About&lt;/strong&gt; section that read like a person rather than a keyword dump -- the difference I measured when I tested the &lt;a href="https://charliemorrison.dev/linkedin-about?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=i-sent-40-linkedin-recruiter-connection-notes" rel="noopener noreferrer"&gt;LinkedIn About builder&lt;/a&gt; -- and an unambiguous open-to-work signal so the recruiter is not guessing whether you are even looking. When I audited 40 open-to-work profiles, the ones that converted browsing recruiters into messages were the ones where the intent was obvious in the first three seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually do now
&lt;/h2&gt;

&lt;p&gt;The forty requests collapsed into a short routine that respects the five-note limit and the one-second scan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Optimize the headline first.&lt;/strong&gt; It is on every invitation whether you write a note or not. Lead with the role and one hard number, tuned for how recruiters search. This is the single edit that moves the most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For most connects, send no note&lt;/strong&gt; -- a clean profile with a strong headline gets accepted, and you keep your five monthly notes in reserve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend a note only when the person is worth one&lt;/strong&gt; , and make it specific: one real reason, one concrete credential, one soft ask, under 200 characters. Never the generic template -- it underperforms silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the accept as step one.&lt;/strong&gt; Plan the short, specific follow-up before you send the request, because the conversation is the actual goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the landing profile hold up&lt;/strong&gt; -- a human About section and a clear open-to-work signal -- so the recruiter you finally got in front of does not bounce.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;Should you add a note to a LinkedIn connection request to a recruiter?&lt;/p&gt;

&lt;p&gt;Only when the note is genuinely specific. In testing, a generic "I'd love to connect" note performed worse than sending no note at all -- it reads as mass outreach and slightly lowers the accept rate. A specific note (one real reason, one concrete credential, one soft ask, under 200 characters) won clearly on reply rate. Free accounts also get only five personalized notes per month, so save them for people who are worth one and send a clean no-note request otherwise.&lt;/p&gt;

&lt;p&gt;How many personalized connection notes can you send on LinkedIn for free?&lt;/p&gt;

&lt;p&gt;Five per month on a free account, each capped at 200 characters, according to LinkedIn's own help documentation. LinkedIn Premium removes the limit. Because notes are scarce on free, the smart move is to send most connection requests with no note (relying on a strong headline) and reserve your five notes for the highest-value recruiters with a genuinely specific message.&lt;/p&gt;

&lt;p&gt;What matters more, the connection note or the profile headline?&lt;/p&gt;

&lt;p&gt;The headline, for most requests. Your name, headline, and current role appear in the invitation notification whether or not you write a note, so they do the accepting-and-replying work by default. A headline that leads with your role and a concrete number was the single clearest accept-rate lift in the test. Optimize the headline first; it is always visible and costs nothing, unlike the five monthly notes.&lt;/p&gt;

&lt;p&gt;What should a connection note to a recruiter actually say?&lt;/p&gt;

&lt;p&gt;Keep it under 200 characters and include three things: one real, checkable reason to connect (a role they posted, a company they recruit for), one concrete credential compressed to a few words ("backend eng, scaled payments to 2M req/min"), and a soft, low-pressure ask ("open to connecting if you work on backend roles"). Avoid cramming a cover letter in -- long notes get truncated or read as desperate. Short and specific beat long and specific.&lt;/p&gt;

&lt;p&gt;The honest summary, after forty requests and two gates: "always personalize" is bad advice taken literally, because a generic personalization is worse than none. What actually works is treating your five monthly notes like the scarce resource they are, making the headline -- the thing recruiters always see -- carry the weight, and spending a real note only when a real reason and a real credential earn it. A connection is not the win. It is a door held open for one short, specific conversation, and everything above is just how to get that conversation started.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/i-sent-40-linkedin-recruiter-connection-notes/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>linkedin</category>
      <category>jobsearch</category>
    </item>
    <item>
      <title>What Are Your Salary Expectations? I Tested 12 Answers</title>
      <dc:creator>charlie-morrison</dc:creator>
      <pubDate>Wed, 01 Jul 2026 20:09:32 +0000</pubDate>
      <link>https://dev.to/charliemorrison/what-are-your-salary-expectations-i-tested-12-answers-4j65</link>
      <guid>https://dev.to/charliemorrison/what-are-your-salary-expectations-i-tested-12-answers-4j65</guid>
      <description>&lt;p&gt;There is a piece of advice about the salary-expectations question that gets repeated in every career thread until it sounds like settled law: &lt;em&gt;never name a number first&lt;/em&gt;. Deflect. Turn it back on them. Whoever says a figure first loses. I believed it too, right up until an application form refused to submit because the "Desired salary" field was required and would not accept the word "Negotiable." That little red asterisk does not care about your negotiating theory. So instead of trusting the folklore one more time, I ran the question through an actual test: I answered &lt;strong&gt;" what are your salary expectations?"&lt;/strong&gt; twelve different ways, across the three places it actually gets asked, and tracked what each answer did to the conversation that followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: four strategies, three channels
&lt;/h2&gt;

&lt;p&gt;The question is not one situation, it is three, and they behave nothing alike. It shows up as a &lt;strong&gt;required field on an application form&lt;/strong&gt; , as the closing question on a &lt;strong&gt;recruiter phone screen&lt;/strong&gt; , and as a mid-conversation probe from the &lt;strong&gt;hiring manager&lt;/strong&gt; once you are deeper in. I took four common strategies and ran each one through all three channels -- twelve combinations in total -- using my own job search plus a handful of applications volunteers let me run on their behalf, all for the same kind of senior backend role so the market range stayed constant.&lt;/p&gt;

&lt;p&gt;The four strategies were the ones everyone actually argues about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deflect&lt;/strong&gt; -- "I'd want to learn more about the role and scope before putting a number on it."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single number&lt;/strong&gt; -- one specific target figure, stated plainly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Researched range&lt;/strong&gt; -- a band drawn from market data, with the &lt;em&gt;bottom of the range set to my actual target&lt;/em&gt; so the whole range sits above my floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flip it back&lt;/strong&gt; -- "What's the budget allocated for this role?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I scored each attempt on two things that matter more than feeling clever: did the conversation &lt;em&gt;advance&lt;/em&gt; to the next stage, and did the eventual number &lt;em&gt;anchor&lt;/em&gt; at or above my target. An answer that feels powerful but gets you screened out has lost. An answer that feels meek but keeps you in the running and lands the number has won.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the "always deflect" rule falls apart
&lt;/h2&gt;

&lt;p&gt;The first channel broke the rule outright. On &lt;strong&gt;application forms&lt;/strong&gt; , deflection is not a strategy, it is a malfunction. Roughly half the forms I hit made "Desired salary" a required numeric field. You cannot type "negotiable," you cannot leave it blank, and a few systems silently parsed a deliberately low placeholder like &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt; as your real expectation and used it to rank you out at the bottom. The flip-it-back strategy is equally useless here -- there is nobody on the other end of a text field to flip it to. On forms, only the two strategies that produce a number survive, and between them the &lt;strong&gt;researched range&lt;/strong&gt; won every time: a range reads as informed rather than rigid, and because I had set its bottom edge to my true target, even the "low" end of what I typed was still a number I would happily accept.&lt;/p&gt;

&lt;p&gt;This is the part the standard advice never mentions, because the standard advice was written for a phone call and then copied onto situations it does not fit. A required form field is a channel where naming a number first is mandatory, and pretending otherwise just gets your application deprioritized or your placeholder weaponized against you. The honest move is to treat the field as the first anchor of the negotiation and put a real, defensible range in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recruiter screen: deflection survives, but barely
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;recruiter phone screen&lt;/strong&gt; is the one situation where the textbook advice has any footing -- and even there it is weaker than its reputation. Recruiters in 2026 are usually screening for budget fit before they spend a manager's time, so the salary question lands early and the subtext is "are you in our band or are we both wasting an afternoon." A clean deflection ("I'd love to hear the range you're working with first") worked about half the time: some recruiters happily volunteered the band, which is the dream outcome, because then &lt;em&gt;they&lt;/em&gt; anchored. But the other half simply re-asked, slightly cooler, and a couple noted that a candidate who can't give a ballpark "doesn't know their market." Deflect twice and you read as evasive.&lt;/p&gt;

&lt;p&gt;What consistently kept the screen moving was the &lt;strong&gt;researched range with a one-line reason&lt;/strong&gt; -- "Based on the market for this role and my eleven years, I'm targeting the 165 to 185 band; happy to be flexible depending on the full package." It answers the actual question, it signals you have done your homework, and it leaves room. The flip-it-back move had a narrow but real use here: asking for &lt;em&gt;their&lt;/em&gt; band first, once, before giving mine, often got the recruiter to anchor -- but if they bounced it back, I gave the range rather than dig in, because a standoff on a screening call is a fast way to not get a second call. The lesson across this channel was that you can lead with a question, but you cannot &lt;em&gt;refuse&lt;/em&gt; to ever give a number; the range is your fallback the instant the deflection meets resistance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hiring manager: the only place a single number shines
&lt;/h2&gt;

&lt;p&gt;Deeper in -- once a hiring manager is sold on you and the conversation turns to making it real -- the math flips. Here, where you have the upper hand because they want &lt;em&gt;you&lt;/em&gt; specifically, a tighter, more confident answer paid off, and a wide range started to cost money. This is where the famous warning about ranges is actually true: state "165 to 185" to someone who is about to write the offer, and 165 is the number they hear, because the bottom of a range becomes the ceiling of their generosity. At the offer stage I switched to a &lt;strong&gt;single target number with justification&lt;/strong&gt; and let silence do the rest.&lt;/p&gt;

&lt;p&gt;To make that concrete I ran the exact scenario through the &lt;a href="https://charliemorrison.dev/salary-negotiation?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=career&amp;amp;utm_content=i-tested-12-salary-expectation-answers" rel="noopener noreferrer"&gt;salary negotiation script generator&lt;/a&gt; on this site -- a senior backend role, current pay 145K, target 175K, data-driven tone -- to see what a clean, defensible version of the late-stage answer sounds like. Here is what it produced:&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%2Fcvv7j4p61hpp37k4jpq3.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%2Fcvv7j4p61hpp37k4jpq3.png" alt="The salary negotiation script generator on charliemorrison.dev producing two data-driven new-offer scripts for a Senior Backend Engineer targeting 175000 against a 145000 offer, each citing market range and quantified wins, followed by a Negotiation Tips list that includes 'Never give a range. The low end becomes the ceiling' and 'After stating your ask, stop talking.'" width="800" height="1484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generator's late-stage script names one target number, grounds it in market data and quantified wins -- and its own tips warn "never give a range" at the offer table, the exact opposite of what wins on a form.&lt;/p&gt;

&lt;p&gt;Notice the tension in that screenshot, because it is the whole point of this experiment. The tool's tip list says, flatly, &lt;strong&gt;" Never give a range. The low end becomes the ceiling."&lt;/strong&gt; That advice is correct -- for the moment it was written for, which is the offer-table negotiation the generator produces scripts for. It is also exactly wrong for the application form, where a range is the safest possible answer and a bare single number boxes you in before you know anything about the role. The same sentence is good advice and bad advice depending on the channel. That is why "never name a number first" keeps failing people: it is a real rule from one room being shouted into every other room.&lt;/p&gt;

&lt;h3&gt;
  
  
  Walk into the salary question with the script already written
&lt;/h3&gt;

&lt;p&gt;The Job Search AI Toolkit bundles the salary negotiation script generator with the ATS resume checker, keyword extractor, and cover-letter and resume-bullet writers -- so you have a researched range for the form, a clean number for the offer, and the justification for both, before anyone asks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://charliemorrison.lemonsqueezy.com/checkout/buy/0c3f51d6-9089-466e-ada4-58a0b22036e0?utm_source=site&amp;amp;utm_medium=blog&amp;amp;utm_campaign=career&amp;amp;utm_content=i-tested-12-salary-expectation-answers&amp;amp;checkout%5Bcustom%5D%5Bsrc%5D=blog_salary_expectations_post" rel="noopener noreferrer"&gt;Get the Job Search AI Toolkit -- $12&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one input every strategy depended on
&lt;/h2&gt;

&lt;p&gt;None of the four strategies worked without a real number behind it, and the number is the part most people skip. A range you invented in your head is not research, it is a wish, and recruiters can hear the difference instantly. Before any of these answers I anchored every figure to public wage data -- the U.S. Bureau of Labor Statistics publishes occupational wage estimates you can pull for free from its &lt;a href="https://www.bls.gov/bls/blswage.htm" rel="noopener noreferrer"&gt;wage data by area and occupation&lt;/a&gt;, which gives you a defensible floor and median that no recruiter will argue with. On top of that I checked role-specific ranges on the postings themselves (pay-transparency laws now put bands on a lot of listings) and triangulated. The range I quoted was always "market median to market-plus-my-evidence," never a guess.&lt;/p&gt;

&lt;p&gt;That research is also what lets you set the bottom of your range honestly to your target. If the market median for the role is 165 and your real target is 165, then quoting "165 to 185" is not a trick -- it is a true statement where even the worst case keeps you whole. The strategy only works because the homework is real. For the deeper mechanics of &lt;em&gt;when&lt;/em&gt; in the process to reveal information and how to frame the ask, Harvard Business School's Deepak Malhotra wrote the reference that most good advice is quietly cribbed from -- his &lt;a href="https://hbr.org/2014/04/15-rules-for-negotiating-a-job-offer" rel="noopener noreferrer"&gt;15 rules for negotiating a job offer&lt;/a&gt; -- and his core point lines up with what the test showed: your leverage and your information change as you move through the process, so the same question deserves a different answer at each stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually say now
&lt;/h2&gt;

&lt;p&gt;The experiment collapsed twelve attempts into a simple decision tree that depends only on which channel you are in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Application form, required field:&lt;/strong&gt; enter a &lt;em&gt;researched range&lt;/em&gt; with the bottom set to your true target. Never leave it blank, never type a token low number, never write "negotiable" in a numeric field. This is a forced anchor -- make it a good one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruiter screen:&lt;/strong&gt; try once to get &lt;em&gt;their&lt;/em&gt; band first ("What range is budgeted for the role?"). If they give it, you win. If they re-ask, give your &lt;em&gt;researched range with a one-line reason&lt;/em&gt; and move on -- do not deflect twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiring manager / offer stage:&lt;/strong&gt; switch to a &lt;em&gt;single confident number&lt;/em&gt; with a justification, then stop talking. This is the one room where the "never give a range" warning is right, because the low end becomes the ceiling.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What disappeared from my answers entirely was the reflexive deflection. "I'd rather not say" is not a power move in 2026; on forms it is impossible, on screens it reads as evasive, and at the table it just delays the number you will have to give anyway. The skill is not refusing to answer -- it is answering with the right shape for the room. I confirmed the same pattern when I tracked 38 counter-offer negotiations: the offers that moved up were the ones anchored on evidence and stated plainly, not the ones where the candidate played coy. The companion run where I generated 30 negotiation scripts pointed the same direction -- a number with a reason beats a clever dodge almost every time.&lt;/p&gt;

&lt;p&gt;And because the salary question rarely arrives alone -- it usually shows up tangled into a broader screening conversation -- it is worth rehearsing alongside the rest of the round rather than in isolation; the way I prep that whole sequence is in my interview-prep test of 31 strategies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;p&gt;What is the best answer to "what are your salary expectations"?&lt;/p&gt;

&lt;p&gt;It depends on where you're asked. On a required application form, enter a researched range with the bottom set to your true target — never blank, never a token low number. On a recruiter screen, try once to get their budgeted range first, then give your researched range if they re-ask. At the offer stage with a hiring manager, switch to a single confident number with a justification and stop talking. There is no single line that works in all three places.&lt;/p&gt;

&lt;p&gt;Should I give a salary range or a single number?&lt;/p&gt;

&lt;p&gt;A range early (forms and recruiter screens), a single number late (offer negotiation). Early on, a range reads as informed and keeps you in the funnel; set its bottom to your real target so even the low end is acceptable. At the offer table, a range hurts you because the low end becomes the number they offer — there, name one figure and justify it.&lt;/p&gt;

&lt;p&gt;Is it true you should never name a number first?&lt;/p&gt;

&lt;p&gt;No — that advice was written for a live negotiation and copied onto situations it doesn't fit. On an application form the salary field is often required, so you must name a number first or get deprioritized. On a recruiter screen, refusing to give any figure can read as evasive. "Don't anchor first" only really applies once you're deep enough to have real bargaining power, and even then a researched anchor is often better than waiting.&lt;/p&gt;

&lt;p&gt;How do I research a defensible salary range?&lt;/p&gt;

&lt;p&gt;Start with public wage data — the U.S. Bureau of Labor Statistics publishes free occupational wage estimates by area, which gives you a floor and median no recruiter will dispute. Then read the bands on actual postings (pay-transparency laws put ranges on many listings now) and triangulate to a role-specific number. Your quoted range should run from the market median to market-plus-your-own-evidence, never a figure you guessed.&lt;/p&gt;

&lt;p&gt;The honest summary, after twelve attempts: the salary-expectations question is not one question and there is no one answer. The advice that fails people treats a recruiter-call rule as a universal law and then watches it shatter against a required form field. Match the answer to the channel -- range on the form, range-after-one-deflection on the screen, single number at the table -- and anchor every figure to real wage data, and the question stops being a trap. It becomes the first place you get to set the terms.&lt;/p&gt;

&lt;p&gt;If you want the pieces that make this quick under pressure, the salary negotiation script generator writes the offer-stage version with your numbers and tone, so the one room where a single figure matters most is the room you walk into already scripted.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://charliemorrison.dev/blog/i-tested-12-salary-expectation-answers/" rel="noopener noreferrer"&gt;charliemorrison.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was written with AI assistance and links to a free tool I built; the tool has an optional paid upgrade, so I may earn a small commission if you choose it — at no extra cost to you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>career</category>
      <category>job</category>
      <category>interview</category>
    </item>
  </channel>
</rss>
