Skip to content
Back to skills

Aiohttp

ASecurity

Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues

  • 21 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 2, 2026
developmentpythongofastapidjangoflasktestingdebugginggitapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add CodeAtCode/oss-ai-skills --skill aiohttp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Aiohttp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Aiohttp
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/codeatcode-aiohttp/badge)](https://www.skillsdirectory.com/skills/codeatcode-aiohttp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: aiohttp
description: Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues
metadata:
  author: mte90
  version: 2.0.0
  tags:
    - python
    - http
    - async
    - server
    - websocket
    - sse
---

# aiohttp

Asynchronous HTTP client/server framework for Python.

## When to Use aiohttp

**Choose aiohttp when:**
- Building async Python HTTP servers with fine-grained control over routing and middleware
- Need both client and server in one framework with WebSocket/SSE support
- Streaming responses or large file uploads/downloads are required

**Consider alternatives:**
- **fastapi** — When you want automatic OpenAPI docs, Pydantic validation built-in, and simpler syntax
- **httpx** — When you need a modern async HTTP client with HTTP/2 support (better than aiohttp's)
- **Flask/FastAPI + httpx** — For sync codebases (aiohttp is purely async)

## Quick Start: Minimal Server

```python
from aiohttp import web

async def health_check(request):
    return web.json_response({"status": "ok"})

async def create_user(request):
    data = await request.json()
    # Validate with Pydantic or manual checks
    return web.json_response({"id": 1, "username": data["username"]}, status=201)

app = web.Application()
app.router.add_get('/health', health_check)
app.router.add_post('/users', create_user)

if __name__ == '__main__':
    web.run_app(app, host='127.0.0.1', port=8080)
```

## Server-Side Patterns

### Application Setup with Startup/Cleanup Hooks

Use `app.on_startup` and `app.on_cleanup` for resource lifecycle:

```python
from aiohttp import web
import asyncpg

async def init_db(app):
    """Create database connection pool."""
    app['db_pool'] = await asyncpg.create_pool(
        host='localhost',
        port=5432,
        database='app_db'
    )

async def close_db(app):
    """Close database connection pool."""
    await app['db_pool'].close()

app = web.Application()
app.on_startup.append(init_db)
app.on_cleanup.append(close_db)
app.router.add_get('/users', list_users)
```

### Modern Lifespan Context (v3.9+)

For cleaner startup/shutdown with context manager semantics:

```python
from aiohttp import web

async def lifespan_ctx(app):
    """Lifespan context manager for resource management."""
    # Startup
    app['db_pool'] = await create_db_pool()
    app['cache'] = await create_cache()
    
    yield  # App runs here
    
    # Cleanup
    await app['cache'].close()
    await app['db_pool'].close()

app = web.Application()
app.router.add_get('/data', data_handler)
# Run with: web.run_app(app, lifespan=lifespan_ctx)
```

### Middleware Patterns

Middleware wraps request handling for cross-cutting concerns:

```python
from aiohttp import web
import time
import logging

logger = logging.getLogger(__name__)

@web.middleware
async def timing_middleware(request, handler):
    """Track request duration."""
    start = time.perf_counter()
    try:
        response = await handler(request)
        duration = time.perf_counter() - start
        logger.info(f"{request.method} {request.path} {response.status} ({duration:.3f}s)")
        return response
    except Exception as e:
        duration = time.perf_counter() - start
        logger.error(f"{request.method} {request.path} failed after {duration:.3f}s: {e}")
        raise

@web.middleware
async def auth_middleware(request, handler):
    """Authentication middleware."""
    public_paths = ['/health', '/public/']
    if any(request.path.startswith(p) for p in public_paths):
        return await handler(request)
    
    auth_header = request.headers.get('Authorization')
    if not auth_header or not await validate_token(auth_header):
        return web.json_response(
            {"error": "Unauthorized"},
            status=401,
            headers={'WWW-Authenticate': 'Bearer'}
        )
    
    # Attach user info to request
    request['user'] = await decode_token(auth_header)
    return await handler(request)

# Combine middleware (applied left-to-right)
app = web.Application(middlewares=[timing_middleware, auth_middleware])
```

### Response Choices

| Response Type | Use Case | Example |
|---------------|----------|---------|
| `web.Response(text=...)` | Plain text, HTML | `web.Response(text="OK", content_type="text/html")` |
| `web.json_response(...)` | JSON bodies (auto-serializes) | `web.json_response({"key": "value"}, status=201)` |
| `web.StreamResponse()` | Streaming large responses | See streaming section below |
| `web.FileResponse()` | File downloads | `web.FileResponse('data.zip')` |
| `web.HTTPFound()` | Redirects | `web.HTTPFound('/new-location')` |
| HTTP exception classes | Error responses | `web.HTTPBadRequest()`, `web.HTTPNotFound()` |

Manual status setting:
```python
# Explicit status codes
return web.json_response({"error": "not found"}, status=404)
return web.Response(text="Created", status=201)

# HTTP exception classes (automatic status)
raise web.HTTPBadRequest(reason="Invalid input")
return web.HTTPUnauthorized(headers={'WWW-Authenticate': 'Bearer'})
```

### Streaming Responses

For large files or real-time data:

```python
from aiohttp import web
import asyncio

async def stream_data(request):
    """Server-Sent Events style streaming."""
    response = web.StreamResponse(
        status=200,
        headers={
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive'
        }
    )
    await response.prepare(request)
    
    try:
        for i in range(10):
            data = f"data: {{'count': {i}}}\n\n"
            await response.write(data.encode())
            await response.drain()
            await asyncio.sleep(1)
    finally:
        await response.write_eof()
    
    return response

async def stream_file(request):
    """Stream large file in chunks."""
    response = web.StreamResponse()
    response.headers['Content-Type'] = 'application/octet-stream'
    response.headers['Content-Length'] = str(file_size)
    await response.prepare(request)
    
    async with aiofiles.open('large_file.bin', 'rb') as f:
        while chunk := await f.read(8192):
            await response.write(chunk)
    
    return response
```

## Client-Side Patterns

### Basic Client Usage

```python
import aiohttp
import asyncio

async def fetch_data():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://api.example.com/data') as response:
            response.raise_for_status()
            return await response.json()

asyncio.run(fetch_data())
```

### Connection Pooling Configuration

```python
import aiohttp

# Default connector (often insufficient for production)
# session = aiohttp.ClientSession()  # ❌ BAD: Uses defaults

# Production-ready connector
connector = aiohttp.TCPConnector(
    limit=100,              # Total connection pool size (default: 100)
    limit_per_host=30,      # Max connections per host (default: 30)
    ttl_dns_cache=300,      # DNS cache TTL in seconds
    ssl=True,               # Verify SSL certificates
    enable_cleanup_closed=True,  # Clean closed connections
)

timeout = aiohttp.ClientTimeout(
    total=30,       # Total request timeout (connect + transfer)
    connect=5,      # Connection establishment timeout
    sock_connect=5, # Socket connection timeout
    sock_read=10,   # Read timeout (per read operation)
)

session = aiohttp.ClientSession(
    connector=connector,
    timeout=timeout,
    headers={'User-Agent': 'my-app/1.0'}
)
```

### Session Lifecycle

```python
# ✅ GOOD: Reuse session across requests
async def process_multiple_urls(urls):
    timeout = aiohttp.ClientTimeout(total=30)
    connector = aiohttp.TCPConnector(limit=100)
    
    async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return results

async def fetch_url(session, url):
    async with session.get(url) as response:
        response.raise_for_status()
        return await response.json()

# ❌ BAD: Creating session per request (causes socket exhaustion)
async def bad_pattern(urls):
    results = []
    for url in urls:
        async with aiohttp.ClientSession() as session:  # New session each time
            async with session.get(url) as response:
                results.append(await response.json())
        # Session closed immediately after each request
    return results
```

## Anti-Patterns and Failure Modes

### Socket Exhaustion from Session Per Request

**Symptom:** `OSError: [Errno 24] Too many open files` or connection timeouts under load

**Cause:** Creating `ClientSession()` inside request handlers or loops without reuse. Each session maintains its own connection pool and file descriptors.

**Fix:**
```python
# ❌ ANTI-PATTERN: Session created per request
async def handler(request):
    session = aiohttp.ClientSession()  # New session every request
    async with session.get(url) as resp:
        return web.json_response(await resp.json())

# ✅ FIX: Session stored in app, reused across requests
async def init_app():
    app = web.Application()
    app['session'] = aiohttp.ClientSession(
        connector=aiohttp.TCPConnector(limit=100)
    )
    return app

async def cleanup_app(app):
    await app['session'].close()

app = await init_app()
app.on_cleanup.append(cleanup_app)

async def handler(request):
    session = request.app['session']
    async with session.get(url) as resp:
        return web.json_response(await resp.json())
```

### No Total Timeout: Hung Server Holds Connections

**Symptom:** Connections accumulate, eventually hitting `limit` in TCPConnector, new requests hang

**Cause:** Missing `ClientTimeout` means no total timeout. A slow or hung server can hold connections indefinitely.

**Fix:**
```python
# ❌ ANTI-PATTERN: No timeout specified
session = aiohttp.ClientSession()
async with session.get('https://slow-api.com/data') as resp:
    # If server hangs, this waits forever
    data = await resp.json()

# ✅ FIX: Always set timeouts
timeout = aiohttp.ClientTimeout(
    total=30,       # Max total time for entire request
    connect=5,      # Max time to establish connection
    sock_read=10    # Max time between read operations
)
session = aiohttp.ClientSession(timeout=timeout)
```

### Missing Read/Connect Timeouts

**Symptom:** Connection established but data never arrives; or DNS resolution hangs

**Cause:** Only setting `total` timeout isn't enough. `sock_read` and `connect` catch specific failure modes.

**Fix:**
```python
# ❌ ANTI-PATTERN: Only total timeout
timeout = aiohttp.ClientTimeout(total=60)

# ✅ FIX: Granular timeouts
timeout = aiohttp.ClientTimeout(
    total=60,       # Overall request timeout
    connect=5,      # Fail fast if can't connect
    sock_connect=5, # Socket connection timeout
    sock_read=30    # Read timeout (prevents stuck on slow responses)
)
```

### Reusing Session Across Event Loops

**Symptom:** `RuntimeError: Cannot call nested app.handler()` or `RuntimeError: Session is closed`

**Cause:** `ClientSession` is bound to the event loop it was created on. Reusing it after `asyncio.run()` restarts the loop.

**Fix:**
```python
# ❌ ANTI-PATTERN: Global session
session = aiohttp.ClientSession()  # Created at module load

async def main():
    asyncio.run(fetch_data())  # New event loop
    # session is bound to old loop!

# ✅ FIX: Create session within event loop context
async def main():
    async with aiohttp.ClientSession() as session:
        await fetch_data(session)

asyncio.run(main())
```

### Not Releasing Response Resources

**Symptom:** Memory growth, connection pool depletion over time

**Cause:** Not using `async with` or not calling `response.release()` leaves connections in limbo.

**Fix:**
```python
# ❌ ANTI-PATTERN: Not consuming response
async with session.get(url) as response:
    # Forgot to read/release
    pass
# Response may not be fully released

# ✅ FIX: Always consume or explicitly release
async with session.get(url) as response:
    data = await response.read()  # Consume fully
# OR
async with session.get(url) as response:
    if response.status != 200:
        response.release()  # Explicit release for early exit
        raise Exception(f"Unexpected status: {response.status}")
```

## Client/Server Selection Guidance

### When to Tune TCPConnector

**Plain `ClientSession` is fine when:**
- Making occasional requests (< 10/second)
- Single host API calls
- Development/testing

**Tune `TCPConnector` when:**
- High throughput (> 100 req/s) → increase `limit` and `limit_per_host`
- Calling many different hosts → increase `limit`, keep `limit_per_host` moderate
- Connection errors under load → enable `enable_cleanup_closed=True`
- DNS lookups are slow → set `ttl_dns_cache=300` or higher

```python
# High-throughput single API
connector = aiohttp.TCPConnector(
    limit=500,
    limit_per_host=100,
    ttl_dns_cache=300
)

# Many different hosts (aggregator pattern)
connector = aiohttp.TCPConnector(
    limit=1000,
    limit_per_host=10,  # Don't overwhelm any single host
    ttl_dns_cache=600
)
```

### When aiohttp is Wrong

**Use httpx instead when:**
- Need HTTP/2 support (aiohttp's is experimental)
- Want sync API alongside async (httpx provides both)
- Using libraries that don't play well with aiohttp's connector

**Use sync HTTP client (requests) when:**
- Codebase is synchronous
- Integration with sync frameworks (Flask without ASGI, Django sync views)
- HTTP/2 not required and simplicity preferred

## Testing

See `references/testing.md` for pytest-aiohttp fixtures, test client usage, and lifespan testing patterns.

## Deep Dives

Load these reference files on demand for specific topics:

- **Middleware & Auth** — `references/middleware.md`: Global error handlers, logging, CORS, rate limiting, JWT/basic auth patterns (when implementing cross-cutting concerns)
- **Client & Performance** — `references/client.md`: WebSocket client, streaming uploads, compression, keepalive tuning (when optimizing client performance)
- **Testing & Lifespan** — `references/testing.md`: Application signals, lifespan context, test client, pytest-aiohttp fixtures (when writing tests)
- **Troubleshooting** — `references/troubleshooting.md`: Connection refused, timeouts, SSL errors, memory leaks (when debugging issues)

**Official Documentation**: https://docs.aiohttp.org/
**GitHub Repository**: https://github.com/aio-libs/aiohttp
**pytest-aiohttp**: https://pytest-aiohttp.readthedocs.io/

Files in this skill

  • SKILL.md14.5 KB
  • references/client.md5.5 KB
  • references/middleware.md6.9 KB
  • references/testing.md4.8 KB
  • references/troubleshooting.md3.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…