<?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: Phạm Hồng Phúc</title>
    <description>The latest articles on DEV Community by Phạm Hồng Phúc (@peter-present).</description>
    <link>https://dev.to/peter-present</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3865914%2F596029bf-24cc-45e7-80ee-d4873eb38ffe.png</url>
      <title>DEV Community: Phạm Hồng Phúc</title>
      <link>https://dev.to/peter-present</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peter-present"/>
    <language>en</language>
    <item>
      <title>Redis's Event-Driven Architecture and the ae Event Loop</title>
      <dc:creator>Phạm Hồng Phúc</dc:creator>
      <pubDate>Fri, 12 Jun 2026 08:38:01 +0000</pubDate>
      <link>https://dev.to/peter-present/rediss-event-driven-architecture-and-the-ae-event-loop-ica</link>
      <guid>https://dev.to/peter-present/rediss-event-driven-architecture-and-the-ae-event-loop-ica</guid>
      <description>&lt;p&gt;One of the most common questions about Redis is: "Redis is single-threaded, so how can it handle thousands of concurrent connections?". But the more interesting question is: “Why would we need thousands of threads to handle thousands of connections in the first place?”&lt;br&gt;
The answer lies in understanding the difference between &lt;strong&gt;doing work&lt;/strong&gt; and &lt;strong&gt;waiting for work&lt;/strong&gt;. A connection spends most of its lifetime waiting for data to arrive from the network. Waiting is not computation. If a server creates one thread for every connection, many of those threads spend most of their time blocked on I/O operations. Although blocked threads consume little CPU time, they still require memory for their stacks and introduce scheduling overhead.&lt;br&gt;
Redis avoids this problem by using an &lt;strong&gt;event-driven architecture built on I/O multiplexing&lt;/strong&gt;. Instead of dedicating one thread to each connection, a single thread asks the operating system: “Which connections are actually ready for work right now?”. The thread then processes only those connections.&lt;/p&gt;
&lt;h3&gt;
  
  
  Blocking I/O - The traditional model
&lt;/h3&gt;

&lt;p&gt;The simplest server implementation uses blocking I/O:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;   &lt;span class="c1"&gt;// wait for new connection&lt;/span&gt;
    &lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                  &lt;span class="c1"&gt;// read, process, reply&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// only then accept the next client&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design has an obvious limitation. While handle_client() is waiting for a client to send data, the entire server is blocked. No other connections can be accepted or processed. A traditional solution is to create one thread per connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
    &lt;span class="n"&gt;pthread_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handle_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This model can work well at small scales. However, with thousands of concurrent connections, the overhead becomes significant. On many Linux systems, each thread reserves several megabytes of stack space by default. In addition, the OS must continually schedule and switch between threads, causing context-switch overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-blocking I/O
&lt;/h3&gt;

&lt;p&gt;Instead of allowing read() to block until data becomes available, a file descriptor can be configured non-blocking mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F_SETFL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;O_NONBLOCK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; 
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;errno&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;EAGAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="c1"&gt;// No data available yet &lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem now becomes determining when to try again. Continuously checking every connection would waste CPU resources. This approach, known as busy waiting, keeps the CPU fully occupied even when no useful work is being performed. What is needed is a mechanism that allows the operating system to notify the application only when a file descriptor becomes ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  I/O multiplexing
&lt;/h3&gt;

&lt;p&gt;I/O multiplexing enables a single thread to monitor many file descriptors simultaneously.&lt;/p&gt;

&lt;h4&gt;
  
  
  select(): The first generation
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FD_ZERO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Block until at least one fd is ready&lt;/span&gt;
&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_fd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Then scan everything to find which ones are ready&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FD_ISSET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;select()&lt;/strong&gt; has two major limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is typically limited to &lt;strong&gt;FD_SETSIZE&lt;/strong&gt; file descriptors (often 1024).&lt;/li&gt;
&lt;li&gt;Each invocation requires scanning the entire set of descriptors, resulting in O(n) complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;pool()&lt;/strong&gt; removes the fixed descriptor limit, but it still requires scanning all registered descriptors after each call.&lt;br&gt;
&lt;strong&gt;epoll&lt;/strong&gt; (in Linux) or &lt;strong&gt;kqueue&lt;/strong&gt; (on maxos/bsd with an equivalent design) solves both problems by inverting the design: instead of handing the kernel a list to check on every call, you register once, and the kernel only notifies you about fds that actually have events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client_fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="n"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; 
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key advantage is that &lt;strong&gt;epoll_wait()&lt;/strong&gt; returns only the file descriptors that are ready. If 10,000 connections are registered but only three receive data, Redis processes only those three connections instead of scanning all 10,000.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ae event library
&lt;/h3&gt;

&lt;p&gt;Redis does not use libraries such as &lt;strong&gt;libevent&lt;/strong&gt; or &lt;strong&gt;libuv&lt;/strong&gt;. Instead, Redis implements its own lightweight event library called ae (A simple Event Library, &lt;a href="https://github.com/redis/redis/blob/unstable/src/ae.c" rel="noopener noreferrer"&gt;https://github.com/redis/redis/blob/unstable/src/ae.c&lt;/a&gt;). The central data structure is &lt;strong&gt;aeEventLoop&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;aeEventLoop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;setsize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="n"&gt;aeFileEvent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="n"&gt;aeFiredEvent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="n"&gt;aeTimeEvent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timeEventHead&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="n"&gt;aeApiState&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;apidata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;aeEventLoop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, the event loop consists of three parts:&lt;/p&gt;

&lt;p&gt;aeEventLoop&lt;br&gt;
├── File events&lt;br&gt;
│   ├── acceptTcpHandler&lt;br&gt;
│   ├── readQueryFromClient&lt;br&gt;
│   └── sendReplyToClient&lt;br&gt;
│&lt;br&gt;
├── Time events&lt;br&gt;
│   └── serverCron&lt;br&gt;
│&lt;br&gt;
└── Backend API&lt;br&gt;
    └── epoll / kqueue / select&lt;/p&gt;

&lt;p&gt;File events handle socket activity. Time events execute periodic tasks that Redis must perform regardless of network activity. The backend API abstracts platform-specific multiplexing mechanisms.&lt;/p&gt;
&lt;h3&gt;
  
  
  The main event loop
&lt;/h3&gt;

&lt;p&gt;Redis spends most of its lifetime executing the following loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;aeMain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aeEventLoop&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;eventLoop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="n"&gt;eventLoop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;eventLoop&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;aeProcessEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eventLoop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;AE_ALL_EVENTS&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
        &lt;span class="n"&gt;AE_CALL_BEFORE_SLEEP&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; 
        &lt;span class="n"&gt;AE_CALL_AFTER_SLEEP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually, each iteration follows this sequence:&lt;/p&gt;

&lt;p&gt;aeMain()&lt;br&gt;
    ↓&lt;br&gt;
aeProcessEvents()&lt;br&gt;
    ↓&lt;br&gt;
epoll_wait() / kqueue()&lt;br&gt;
    ↓&lt;br&gt;
Process file events&lt;br&gt;
    ↓&lt;br&gt;
Process time events&lt;br&gt;
    ↓&lt;br&gt;
Repeat forever&lt;/p&gt;

&lt;p&gt;This design allows Redis to react efficiently to both incoming network requests and scheduled maintenance tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  File events and time events
&lt;/h3&gt;

&lt;p&gt;Redis supports two categories of events.&lt;/p&gt;

&lt;h4&gt;
  
  
  File events
&lt;/h4&gt;

&lt;p&gt;File events are triggered by socket activity. Examples include: &lt;strong&gt;acceptTcpHandler&lt;/strong&gt;, &lt;strong&gt;readQueryFromClient&lt;/strong&gt;, &lt;strong&gt;sendReplyToClient&lt;/strong&gt;. These handlers manage client connections and network communication.&lt;/p&gt;

&lt;h4&gt;
  
  
  Time events
&lt;/h4&gt;

&lt;p&gt;Time events execute periodically. The most important example is &lt;strong&gt;serverCron()&lt;/strong&gt;. By default, Redis executes serverCron() approximately every 100 milliseconds (determined by the &lt;strong&gt;hz&lt;/strong&gt; configuration parameter). &lt;strong&gt;serverCron()&lt;/strong&gt; performs tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running the active expiration cycle&lt;/li&gt;
&lt;li&gt;Collecting statistics&lt;/li&gt;
&lt;li&gt;Managing client timeouts&lt;/li&gt;
&lt;li&gt;Maintaining replication state,&lt;/li&gt;
&lt;li&gt;Performing persistence-related housekeeping,&lt;/li&gt;
&lt;li&gt;Executing cluster maintenance tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without time events, Redis would respond only to network activity and could not perform background maintenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full lifecycle of a request inside ae event loop
&lt;/h3&gt;

&lt;p&gt;To make it concrete, trace a SET key value from start to finish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 1 (server start)&lt;/strong&gt;: During initialization, Redis registers the listening socket: &lt;em&gt;server socket → acceptTcpHandler&lt;/em&gt;. Whenever a new connection arrives, the event loop invokes &lt;strong&gt;acceptTcpHandler()&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2 (client connects)&lt;/strong&gt;: When &lt;strong&gt;epoll_wait()&lt;/strong&gt; reports that the server socket is ready: &lt;em&gt;accept() → new client fd → aeCreateFileEvent(..., readQueryFromClient)&lt;/em&gt;. Redis registers a readable file event for the client socket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3 (receive the command)&lt;/strong&gt;: The client sends &lt;strong&gt;3\r\n$3\r\nSET\r\n...&lt;/strong&gt;. The event loop detects that the client socket is readable and invokes: &lt;strong&gt;readQueryFromClient()&lt;/strong&gt;. The command is copied into: &lt;strong&gt;client→querybuf&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 4 (parse and execute)&lt;/strong&gt;: Redis parses the RESP protocol, identifies the SET command, locates the appropriate command implementation, and executes it. The reply is generated in memory and appended to the client's output buffer. &lt;strong&gt;No I/O happens at this step&lt;/strong&gt;, it is purely a memory operation.&lt;/li&gt;
&lt;li&gt;Step 5 (send the reply): In the same loop iteration (after processing all read events), Redis calls the write handler to &lt;strong&gt;write()&lt;/strong&gt; the reply buffer to the socket. If the reply buffer is large and cannot be flushed in one call, Redis registers a write event handler to continue flushing on the next iteration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Single-Threaded Execution Works Well
&lt;/h3&gt;

&lt;p&gt;A common misconception is that more threads always improve performance. Additional threads are beneficial primarily when the workload is limited by available CPU resources. Many Redis operations, such as &lt;strong&gt;GET&lt;/strong&gt; and &lt;strong&gt;SET&lt;/strong&gt;, perform relatively little computation: &lt;em&gt;lookup key → retrieve value from memory → generate response&lt;/em&gt;. For these workloads, using multiple execution threads can increase overhead due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronization mechanisms protecting shared data,&lt;/li&gt;
&lt;li&gt;context switching performed by the operating system,&lt;/li&gt;
&lt;li&gt;cache coherence traffic between CPU cores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By executing commands in a single thread, Redis avoids these costs entirely. This design simplifies the implementation and enables very high throughput for typical in-memory workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  The limits of this model
&lt;/h3&gt;

&lt;p&gt;Single-threading has one clear weakness: one blocking command blocks the entire server.&lt;/p&gt;

&lt;p&gt;KEYS * on a database with 10 million keys → O(n) scan → every other client waits for the entire duration. This is why &lt;strong&gt;KEYS&lt;/strong&gt; is banned in production and replaced by &lt;strong&gt;SCAN&lt;/strong&gt; (cursor-based, scanning a small portion per call).&lt;/p&gt;

&lt;p&gt;Similarly: LRANGE mylist 0 -1 on a list with a million elements, SORT without LIMIT, SMEMBERS on a huge set — all commands that can stall the event loop.&lt;/p&gt;

&lt;p&gt;Redis 6.0 partially addressed this with threaded I/O: still single-threaded for command execution, but uses multiple threads for reading and writing sockets. The reason: at high connection rates, read() and write() syscalls start consuming a meaningful share of time relative to command execution. Threaded I/O lets Redis exploit multiple cores without breaking the data model.&lt;/p&gt;

&lt;p&gt;Redis 7.0 goes further with Redis Cluster sharding, distributing both data and load across multiple processes — each process still single-threaded — scaling out rather than up.&lt;/p&gt;

</description>
      <category>redis</category>
    </item>
    <item>
      <title>What actually happens when a Redis client connects?</title>
      <dc:creator>Phạm Hồng Phúc</dc:creator>
      <pubDate>Thu, 11 Jun 2026 13:53:24 +0000</pubDate>
      <link>https://dev.to/peter-present/what-actually-happens-when-a-redis-client-connects-57n4</link>
      <guid>https://dev.to/peter-present/what-actually-happens-when-a-redis-client-connects-57n4</guid>
      <description>&lt;p&gt;Nowadays, almost all Redis deployments use TCP as the primary connection protocol. Redis also supports &lt;strong&gt;Unix Domain Socket (UDS)&lt;/strong&gt; when the client and server run on the same machine. UDS bypasses the TCP/IP stack and network interface entirely, typically reducing latency by 30–40% compared to TCP localhost — though the exact gain depends on workload. Because UDS requires co-location, TCP remains the universal default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Redis uses TCP?
&lt;/h3&gt;

&lt;p&gt;TCP fits Redis for the following reasons&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent, stateful connections&lt;/strong&gt;: Redis processes one command at a time per client, in strict order: the client sends a command, waits for the reply, then sends the next. This request-response model requires a persistent, stateful connection, which TCP provides. Each TCP connection is uniquely identified by a 4-tuple (src_ip, src_port, dst_ip, dst_port), letting Redis maintain per-client state: current database index, transaction state (MULTI/EXEC), subscription lists, and so on. UDP is connectionless and stateless; a server would have to re-identify the client on every single datagram, pushing all that state management into application code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-order delivery&lt;/strong&gt;: Redis uses RESP (Redis Serialization Protocol), which is a stream-based protocol. Commands arrive as a continuous byte stream, and Redis parses them sequentially. If bytes arrived out of order, the parser would break. TCP guarantees the stream is always ordered and complete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: TCP automatically retransmits lost packets. Redis does not need to write any retry logic itself; the OS handles it transparently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow control&lt;/strong&gt;: TCP's sliding window prevents a fast client from overwhelming Redis's read buffer. Every TCP packet Redis sends back to the client carries an &lt;strong&gt;rwnd&lt;/strong&gt; (receive window) value, a number the OS stamps automatically, representing free space remaining in the buffer. The client treats this as a hard cap on how much data it can have in flight. As Redis reads and drains its buffer, &lt;strong&gt;rwnd&lt;/strong&gt; grows, and the client can send more. If Redis falls behind, &lt;strong&gt;rwnd&lt;/strong&gt; shrinks toward zero, and the client throttles itself automatically. Redis never writes a line of code for this — the kernel manages it entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelining&lt;/strong&gt;: Because TCP preserves byte order across the entire stream, clients can send many commands in one batch without waiting for individual replies. The server reads commands back-to-back from the stream and sends replies in the same order. This is pipelining, and it would be impossible without a reliable, ordered byte stream.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2F4cdhibh67azcykgip1kd.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.amazonaws.com%2Fuploads%2Farticles%2F4cdhibh67azcykgip1kd.png" alt=" " width="800" height="798"&gt;&lt;/a&gt;&lt;br&gt;
The above diagram describes three phases in Redis server when a Redis client connects for the first time&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accept phase&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;The OS and Redis work asynchronously. When a client calls &lt;strong&gt;connect(),&lt;/strong&gt; the OS kernel handles the entire TCP handshake (SYN → SYN-ACK → ACK) on its own, Redis is not involved. Once the handshake completes, the connection is pushed into the accept queue, sitting there until Redis is ready to pick it up.&lt;/li&gt;
&lt;li&gt;Meanwhile, Redis may be busy executing a command for another client. When it finishes, the event loop calls &lt;strong&gt;epoll_wait()&lt;/strong&gt;. If a connection is waiting in the queue, epoll reports it, and only then does Redis call &lt;strong&gt;accept()&lt;/strong&gt;. If Redis is already idle, it calls &lt;strong&gt;accept()&lt;/strong&gt; almost immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register phase&lt;/strong&gt;: &lt;strong&gt;accept()&lt;/strong&gt; returns a new file descriptor, an integer that uniquely identifies the connection (for example, fd = 7). Redis registers this fd with &lt;strong&gt;epoll&lt;/strong&gt; (Linux) or &lt;strong&gt;kqueue&lt;/strong&gt; (macOS/BSD). From this point, the OS automatically notifies Redis when data arrives on that fd, so Redis never has to poll in a loop. (To be more understandable, you should read about AE Event Loop).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocate phase&lt;/strong&gt;: Redis allocates a client struct in memory for the connection. This includes a 16 KB read buffer to hold incoming command bytes streaming in over TCP, and a write buffer to hold responses waiting to be sent back. The buffer exists because TCP can split a single command like &lt;em&gt;SET key value&lt;/em&gt; across multiple small segments — Redis collects the bytes until it has a complete command before parsing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The cost of a connection
&lt;/h3&gt;

&lt;p&gt;Opening a connection requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A file descriptor (an integer in the kernel)&lt;/li&gt;
&lt;li&gt;A client struct in Redis memory, including the 16 KB read buffer and write buffer — roughly ~20 KB of RAM per connection in total&lt;/li&gt;
&lt;li&gt;A slot in epoll's interest list

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;epoll&lt;/strong&gt; is a Linux kernel mechanism for monitoring multiple file descriptors simultaneously. Redis uses it to know when a client sends data — without constantly polling ("anything yet? anything yet?").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;epoll&lt;/strong&gt; maintains an interest list inside the kernel — essentially a table of file descriptors that Redis has registered and wants to be notified about. Each entry in that table is a slot, containing: the file descriptor to watch (e.g. fd = 7), the event type to listen for (e.g. EPOLLIN — data is ready to read), and a pointer back to the corresponding client struct in Redis memory.&lt;/li&gt;
&lt;li&gt;When Redis calls &lt;strong&gt;epoll_ctl&lt;/strong&gt;(ADD, fd) during the Register phase, it is essentially telling the kernel: "add this fd to the interest list, and notify me when it has data."&lt;/li&gt;
&lt;li&gt;Each slot occupies a small amount of kernel memory (a few dozen bytes). More importantly, epoll has a limit on how many fds it can watch simultaneously — a limit typically bounded by &lt;strong&gt;ulimit -n&lt;/strong&gt; at the OS level. So every new connection doesn't just cost RAM on the Redis side; it also consumes a finite slot in the kernel's interest list.
In systems with thousands of clients, microservices, workers, cron jobs, if every service opens its own dedicated connection, it is easy to hit Redis's maxclients limit (default: 10000) or the OS-level ulimit -n. The solution is connection pooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Connection Pooling
&lt;/h3&gt;

&lt;p&gt;A connection pool is a group of TCP connections that are pre-created and reused. Instead of an application running &lt;strong&gt;connect() → use → close()&lt;/strong&gt; on every request, the pool keeps connections alive and lends them out as needed. Two key configuration values to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;minIdle&lt;/strong&gt; — the minimum number of connections kept ready at all times. This reduces latency spikes when traffic ramps up suddenly, since connections are already warm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;maxTotal&lt;/strong&gt; — the upper limit on total connections in the pool. This prevents a single application from exhausting Redis's connection slots.
Notice: connection pooling works best for long-running processes. In serverless or ephemeral environments where instances spin up and down frequently, persistent pooled connections can actually cause more churn,  you may need a different strategy such as a sidecar proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pipelining
&lt;/h3&gt;

&lt;p&gt;Normally, each command waits for the previous reply before the next command is sent. If the round-trip time (RTT) between client and server is 1 ms, 100 sequential commands take 100 ms of network wait time, even though each command executes in under 1 µs on the server.&lt;/p&gt;

&lt;p&gt;Pipelining solves this by sending multiple commands in a single write, without waiting for each response. The client batches commands at the application layer; TCP delivers them in order; Redis reads and executes them sequentially and sends back all replies in one go. The result: 100 commands might complete in just over 1 ms instead of 100 ms.&lt;/p&gt;

&lt;p&gt;One common misconception: pipelining is not a transaction. Commands are executed in order, but if command A fails, command B still executes. There is no atomicity. If you need all-or-nothing semantics, use &lt;strong&gt;MULTI/EXEC&lt;/strong&gt; or a Lua script instead.&lt;/p&gt;

&lt;p&gt;You can read the full details in the &lt;a href="https://redis.io/docs/latest/develop/using-commands/pipelining/" rel="noopener noreferrer"&gt;official Redis pipelining documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>tcp</category>
    </item>
  </channel>
</rss>
