<?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: Linh Fish Dev</title>
    <description>The latest articles on DEV Community by Linh Fish Dev (@linhfishdev).</description>
    <link>https://dev.to/linhfishdev</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%2F653872%2F583ca1a7-b675-40b3-a272-07298d94bb05.png</url>
      <title>DEV Community: Linh Fish Dev</title>
      <link>https://dev.to/linhfishdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/linhfishdev"/>
    <language>en</language>
    <item>
      <title>Django: Send Email with send_mail() in 5 Minutes — Configure SMTP the Right Way</title>
      <dc:creator>Linh Fish Dev</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:12:36 +0000</pubDate>
      <link>https://dev.to/linhfishdev/django-send-email-with-sendmail-in-5-minutes-configure-smtp-the-right-way-4ign</link>
      <guid>https://dev.to/linhfishdev/django-send-email-with-sendmail-in-5-minutes-configure-smtp-the-right-way-4ign</guid>
      <description>&lt;p&gt;Django already ships with &lt;code&gt;send_mail()&lt;/code&gt; — configure SMTP once in &lt;code&gt;settings.py&lt;/code&gt; and you can send email with a single call. Use it for welcome emails after registration, order confirmations, password resets, or system notifications. No extra library to install, and no hand-rolled SMTP code with &lt;code&gt;smtplib&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Many new projects call &lt;code&gt;send_mail()&lt;/code&gt; right away without configuring anything, assuming Django “just knows” where to send mail. In reality Django defaults to an SMTP backend pointing at &lt;code&gt;localhost:25&lt;/code&gt; — most dev machines have no mail server, so the request hangs and then fails with &lt;code&gt;ConnectionRefusedError&lt;/code&gt;, and users who just registered wait forever for a welcome email that never arrives. Another common mistake is hardcoding the email password straight into &lt;code&gt;settings.py&lt;/code&gt; and accidentally committing it to Git — the mailbox gets exposed and abused to send spam.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.mail&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;send_mail&lt;/span&gt;

&lt;span class="nf"&gt;send_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chao mung ban den voi MyShop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cam on ban da dang ky tai khoan.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noreply@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;khach@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;fail_silently&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ConnectionRefusedError: [Errno 111] Connection refused (localhost:25)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The right approach is to move the configuration into environment variables and let Django handle the SMTP part. In &lt;code&gt;settings.py&lt;/code&gt;, declare the SMTP backend with its host, port, and credentials — the password comes from an environment variable, never written directly into the code. For local development, switch to the console backend so mail is printed to the terminal instead of actually sent, and you never risk emailing real users by accident.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;EMAIL_BACKEND&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;django.core.mail.backends.smtp.EmailBackend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMAIL_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smtp.gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;EMAIL_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;
&lt;span class="n"&gt;EMAIL_USE_TLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;EMAIL_HOST_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMAIL_HOST_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;EMAIL_HOST_PASSWORD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMAIL_HOST_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;DEFAULT_FROM_EMAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyShop &amp;lt;noreply@example.com&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Dev local: in mail ra terminal thay vi gui that
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, anywhere in your code — a view, a signal, or a management command — one &lt;code&gt;send_mail()&lt;/code&gt; call with four arguments is enough: subject, plain-text body, sender address, and the list of recipients. The function returns the number of emails sent successfully, usually &lt;code&gt;1&lt;/code&gt;; if SMTP fails and &lt;code&gt;fail_silently=False&lt;/code&gt; (the default), Django raises an exception so you can handle it instead of silently swallowing the error. For a nicer HTML version, pass &lt;code&gt;html_message&lt;/code&gt; — Django automatically attaches the plain-text body as a fallback for old mail clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.core.mail&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;send_mail&lt;/span&gt;

&lt;span class="n"&gt;sent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;send_mail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Don hang #123 da duoc xac nhan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Don hang #123 cua ban da duoc xac nhan.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;noreply@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;khach@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;html_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Don hang #123&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Cam on ban da mua hang.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Apply It to Your Project
&lt;/h2&gt;

&lt;p&gt;Wire &lt;code&gt;send_mail()&lt;/code&gt; into the right touchpoints: a welcome email after user registration (combined with the &lt;code&gt;post_save&lt;/code&gt; signal), a confirmation email after an order is created, a password-reset email. Sending over SMTP usually takes 1–3 seconds, so don’t call it directly in a view if you don’t want users waiting — push it to a background task or run it after the transaction commits successfully, and always log failures so you can resend later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://linhfish.dev/en/2026/10/13/flask-sending-email-with-flask-mail-dont-let-smtp-block-your-requests-5-minutes/" rel="noopener noreferrer"&gt;Flask: Sending Email with Flask-Mail — Don’t Let SMTP Block Your Requests (5 Minutes)&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://linhfish.dev/2026/09/30/fastapi-xu-ly-viec-nang-o-background-voi-backgroundtasks-client-khong-phai-cho/" rel="noopener noreferrer"&gt;FastAPI: Handle Heavy Work in the Background with BackgroundTasks — Clients Don’t Have to Wait&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://linhfish.dev/en/2026/10/15/django-run-tasks-after-successful-commit-with-transaction-on_commit-in-5-minutes/" rel="noopener noreferrer"&gt;Django: Run Tasks After Successful Commit with transaction.on_commit in 5 Minutes&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I send email with Gmail?
&lt;/h3&gt;

&lt;p&gt;Yes. Use host &lt;code&gt;smtp.gmail.com&lt;/code&gt;, port 587, enable TLS, and use an App Password instead of your main password — Gmail has blocked sign-in with regular passwords since 2022. Remember to turn on two-step verification before creating an App Password.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test sending email without actually sending it?
&lt;/h3&gt;

&lt;p&gt;Set &lt;code&gt;EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'&lt;/code&gt; in your dev settings — mail will be printed to the terminal instead of leaving your machine. To assert on mail in tests, use the locmem backend and inspect &lt;code&gt;django.core.mail.outbox&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can send_mail() send to many people at once?
&lt;/h3&gt;

&lt;p&gt;Yes — pass a list of addresses to &lt;code&gt;recipient_list&lt;/code&gt; and they all receive the same email. If you need to send a bulk batch where each person gets different content, use &lt;code&gt;send_mass_mail()&lt;/code&gt; to open a single SMTP connection instead of reopening one per email.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if my emails keep landing in spam?
&lt;/h3&gt;

&lt;p&gt;Check three things: the sender address must belong to a domain you own, that domain must have SPF/DKIM/DMARC configured, and the content should avoid spam-like wording. Also, sending from a low-reputation SMTP account is more likely to get filtered — consider a dedicated email-sending service when your volume grows.&lt;/p&gt;

&lt;p&gt;💬 &lt;strong&gt;Have you tried this in your own project&lt;/strong&gt;? If you hit an error or know a shorter way to do it, leave a comment below — I read every one and will fold the best ideas into the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Django documentation — Sending email: &lt;a href="https://docs.djangoproject.com/en/stable/topics/email/" rel="noopener noreferrer"&gt;https://docs.djangoproject.com/en/stable/topics/email/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Django documentation — django.core.mail: &lt;a href="https://docs.djangoproject.com/en/stable/topics/email/#send-mail" rel="noopener noreferrer"&gt;https://docs.djangoproject.com/en/stable/topics/email/#send-mail&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linhfish.dev/en/2026/11/07/django-send-email-with-send_mail-in-5-minutes-configure-smtp-the-right-way/" rel="noopener noreferrer"&gt;linhfish.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>email</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Python: Call Multiple APIs in Parallel with asyncio.gather and Semaphore in 10 Minutes</title>
      <dc:creator>Linh Fish Dev</dc:creator>
      <pubDate>Sat, 07 Nov 2026 10:03:43 +0000</pubDate>
      <link>https://dev.to/linhfishdev/python-call-multiple-apis-in-parallel-with-asynciogather-and-semaphore-in-10-minutes-2n8l</link>
      <guid>https://dev.to/linhfishdev/python-call-multiple-apis-in-parallel-with-asynciogather-and-semaphore-in-10-minutes-2n8l</guid>
      <description>&lt;p&gt;When you need to call dozens of APIs — syncing data, crawling prices, firing webhooks — a sequential &lt;code&gt;for&lt;/code&gt; loop adds up the waiting time of every request: 20 requests × 1 second = 20 seconds. &lt;code&gt;asyncio.gather&lt;/code&gt; runs many coroutines concurrently inside a single event loop, while &lt;code&gt;asyncio.Semaphore&lt;/code&gt; limits how many requests fly at the same time so you don't overwhelm the partner API. This technique applies to every I/O-bound task (HTTP calls, file reads, database queries) and only needs the &lt;code&gt;httpx&lt;/code&gt; library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: a Sequential Loop Adds Up All the Waiting
&lt;/h2&gt;

&lt;p&gt;Say you need to fetch info for 20 users from a partner API, each request taking about 1 second. The most "naive" code is a loop with the synchronous &lt;code&gt;httpx.Client&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;URLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;URLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;

&lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;fetch_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finished in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code runs correctly, but the total time is ≈ 20 seconds because each request waits for the previous one to finish. The CPU sits idle almost the whole time — the bottleneck is I/O waiting, not processing power. With 200 records, you'd wait over 3 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: asyncio.gather + Semaphore
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;asyncio&lt;/code&gt; lets many coroutines "suspend" at I/O waiting points and hand the event loop to another coroutine. &lt;code&gt;asyncio.gather(*coros)&lt;/code&gt; groups them and runs them concurrently, returning results in the same order you passed in. But don't fire 200 requests at once — the partner API may rate-limit you or block your IP. &lt;code&gt;asyncio.Semaphore(n)&lt;/code&gt; is a "gate" that lets only &lt;code&gt;n&lt;/code&gt; coroutines through at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# grab a ticket before calling HTTP
&lt;/span&gt;        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_all_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# at most 5 requests at once
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_all_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URLS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How it flows: each &lt;code&gt;fetch_one&lt;/code&gt; grabs a "ticket" from the semaphore before calling HTTP; at most 5 requests fly together, and when one finishes another takes its place. With 20 requests × 1 second and &lt;code&gt;Semaphore(5)&lt;/code&gt;, total time drops to ≈ 4 seconds instead of 20. Measured on a local server (20 requests × 0.3 seconds): 6.19 seconds → 1.41 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collect Errors Neatly with return_exceptions=True
&lt;/h2&gt;

&lt;p&gt;By default, one failing coroutine makes &lt;code&gt;gather&lt;/code&gt; raise the exception immediately, and you lose the results of the requests that already finished. Pass &lt;code&gt;return_exceptions=True&lt;/code&gt; and the error comes back as a normal item in the result list — filter it afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed request:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern shines in data-sync jobs: 98 of 100 records still get processed, and the 2 failures go to the log for a later retry — instead of the whole batch dying because of one bad request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply It to Your Project
&lt;/h2&gt;

&lt;p&gt;Start with &lt;code&gt;Semaphore(5)&lt;/code&gt;, then raise it gradually while watching response times and 429 codes from the partner API. Always set a &lt;code&gt;timeout&lt;/code&gt; on every request — one request hanging forever holds its semaphore "ticket" forever and jams the whole batch. If your project already uses FastAPI, your &lt;code&gt;async def&lt;/code&gt; endpoints can &lt;code&gt;await&lt;/code&gt; &lt;code&gt;fetch_all_async()&lt;/code&gt; directly without &lt;code&gt;asyncio.run()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linhfish.dev/en/2026/10/15/python-stop-using-print-for-production-debugging-log-properly-with-logging-in-5-minutes/" rel="noopener noreferrer"&gt;Python: Stop Using print() for Production Debugging — Log Properly with logging in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linhfish.dev/en/2026/10/17/python-speed-up-repeated-calculations-with-lru_cache-and-cache-in-5-minutes/" rel="noopener noreferrer"&gt;Python: Speed Up Repeated Calculations with @lru_cache and @cache in 5 Minutes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  What should I set Semaphore(n) to?
&lt;/h3&gt;

&lt;p&gt;There's no magic number: start at 5–10, increase gradually, and watch for 429 codes or timeouts from the partner API. Many public APIs limit you to 5–10 requests per second.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does gather keep the result order?
&lt;/h3&gt;

&lt;p&gt;Yes. Results come back in the order you passed the coroutines in, not the order they finish. That's the difference from &lt;code&gt;asyncio.as_completed()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can one bad request kill the whole batch?
&lt;/h3&gt;

&lt;p&gt;By default, yes — &lt;code&gt;gather&lt;/code&gt; raises the first exception it meets. Use &lt;code&gt;return_exceptions=True&lt;/code&gt; to collect errors into the list and handle them later, as shown above.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I NOT use asyncio?
&lt;/h3&gt;

&lt;p&gt;For CPU-bound work (image processing, file compression, heavy math): the GIL prevents coroutines from truly running in parallel. Use &lt;code&gt;multiprocessing&lt;/code&gt; or &lt;code&gt;concurrent.futures.ProcessPoolExecutor&lt;/code&gt; for those.&lt;/p&gt;

&lt;p&gt;💬 &lt;strong&gt;Have you tried this in your project?&lt;/strong&gt; If you hit an error or know a cleaner way, leave a comment below — I read them all and will add the best ideas to this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python docs — asyncio: &lt;a href="https://docs.python.org/3/library/asyncio.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/asyncio.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;httpx docs — Async API: &lt;a href="https://www.python-httpx.org/async/" rel="noopener noreferrer"&gt;https://www.python-httpx.org/async/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://linhfish.dev/en/2026/10/23/python-call-multiple-apis-in-parallel-with-asyncio-gather-and-semaphore-in-10-minutes/" rel="noopener noreferrer"&gt;linhfish.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>httpx</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
