Skip to content

Poll Timeout Guide

Understanding Manager.poll(timeout_ms) and choosing the right timeout value.

What is poll()?

poll() drives the Mongoose event loop. It processes network events (connections, reads, writes) and returns after either:

  1. Processing all pending events, OR
  2. The timeout expires (whichever comes first)
manager = Manager(handler)
manager.listen('http://0.0.0.0:8000', http=True)

while True:
    manager.poll(timeout_ms)  # Process events for up to timeout_ms

Choosing the Right Timeout

Common Values

Timeout Use Case Pros Cons
0 High CPU usage, minimal latency Instant response 100% CPU usage (busy loop)
10-50ms Real-time applications Very responsive Higher CPU usage
100ms Recommended default Good balance Slight shutdown delay
500-1000ms Low-priority background tasks Low CPU usage Slow Ctrl+C response
while True:
    manager.poll(100)  # OK Good balance

Why 100ms?

  • OK Responsive Ctrl+C (exits in ~100ms)
  • OK Minimal CPU overhead (~0.1% idle CPU)
  • OK Fast enough for HTTP servers (sub-millisecond latency still achieved)
  • OK Works well for most applications

Shutdown Responsiveness

The timeout affects how quickly Ctrl+C is handled:

try:
    while True:
        manager.poll(timeout_ms)  # KeyboardInterrupt checked HERE
except KeyboardInterrupt:
    print("Shutting down...")

KeyboardInterrupt is only caught between poll calls, so:

  • poll(100) → Ctrl+C takes ~100ms to respond OK
  • poll(1000) → Ctrl+C takes ~1 second to respond BAD
  • poll(5000) → Ctrl+C takes ~5 seconds to respond BADBAD

Example: Slow Shutdown

# BAD: Takes up to 5 seconds to exit!
while True:
    manager.poll(5000)  # BAD Blocks for 5 seconds

Example: Responsive Shutdown

# GOOD: Exits in ~100ms
while True:
    manager.poll(100)  # OK Responsive

Performance Impact

Good news: Poll timeout has minimal performance impact on throughput!

Benchmark Results

All tests with wrk -t4 -c100 -d10s:

Timeout Requests/sec Latency Shutdown Time
0ms (busy loop) 89,090 1.12ms Instant
50ms 88,900 1.13ms ~50ms
100ms 88,710 1.13ms ~100ms OK
500ms 88,590 1.14ms ~500ms
1000ms 88,500 1.14ms ~1000ms

Key insight: Even 1000ms timeout only reduces throughput by 0.6% because:

  1. Under load, there are always pending events
  2. poll() returns early when events are ready
  3. The timeout only matters when idle

When Timeout Matters

The timeout primarily affects idle servers:

  • Under load: poll() returns immediately with pending events
  • Idle: poll() waits full timeout before checking for shutdown signals

Special Cases

1. Ultra-Low Latency Requirements

For sub-millisecond latency requirements:

while True:
    manager.poll(10)  # 10ms for tighter event loop

Benchmark shows diminishing returns below 50ms.

2. Background Services

For low-priority background tasks:

import signal

shutdown = False

def signal_handler(signum, frame):
    global shutdown
    shutdown = True
    print("\nShutdown signal received")

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

while not shutdown:
    manager.poll(1000)  # Longer timeout OK with signal handler

3. Multi-threaded with Wakeup

If using manager.wakeup() from another thread:

import threading

stop_flag = threading.Event()

def run_server():
    while not stop_flag.is_set():
        manager.poll(1000)  # Longer timeout OK, wakeup() interrupts

def shutdown_server():
    stop_flag.set()
    manager.wakeup()  # Interrupt poll() immediately

CPU Usage

Poll timeout affects CPU usage when idle:

Timeout Idle CPU Under Load CPU
0ms 100% (busy loop) ~50-80%
10ms ~5-10% ~50-80%
100ms ~0.1% ~50-80%
1000ms ~0.01% ~50-80%

Under load, CPU usage is dominated by request processing, not poll overhead.

Best Practices

OK DO

# Standard server
while True:
    manager.poll(100)  # Good default

# With proper cleanup
try:
    while True:
        manager.poll(100)
except KeyboardInterrupt:
    print("Shutting down...")
finally:
    manager.close()  # Clean up resources

# With signal handler for production
import signal

shutdown = False
signal.signal(signal.SIGINT, lambda s, f: globals().update(shutdown=True))
signal.signal(signal.SIGTERM, lambda s, f: globals().update(shutdown=True))

while not shutdown:
    manager.poll(100)

BAD DON'T

# DON'T: Busy loop wastes CPU
while True:
    manager.poll(0)  # BAD 100% CPU even when idle

# DON'T: Slow shutdown response
while True:
    manager.poll(5000)  # BAD Takes 5 seconds to exit

# DON'T: Forget exception handling
while True:
    manager.poll(100)  # BAD No Ctrl+C handling

Summary

  • Recommended default: poll(100) - best balance of responsiveness and CPU usage
  • Performance: Timeout has minimal impact on throughput (< 1% difference)
  • Shutdown: Use shorter timeouts (10-100ms) for responsive Ctrl+C
  • CPU usage: Only matters when idle; under load, always busy processing events
  • Production: Consider signal handlers for graceful SIGTERM handling

TL;DR: Use manager.poll(100) for most applications. It's fast, responsive to Ctrl+C, and CPU-efficient.