Skip to content

About

Idempotent request handling for async Python applications

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

57 Commits

Folders and files

idempotency-kit

Idempotency keys for async Python: one execution per key, and the stored result for every retry.

PyPI Python License CI codecov Docs

Wrap a use case, a job or a message handler, and a repeated call with the same key gets the stored result instead of running again. A retry that arrives while the first call is still running waits for it, or gets a 409. Results live in Redis. It is a result cache with an in-flight reservation, not a distributed transaction: see what it guarantees.

Tip

Building this with an AI assistant? Hand it one page instead of the whole site: the complete API surface, the rules that break code when they are broken — what the key actually identifies, what two concurrent callers really do, how a TTL in seconds is rounded — the mistakes models make with this API, and a map of which page to fetch for the rest. Every docs page is also served as raw Markdown at its own URL, and a Copy page button at the top of each one hands it straight to a chat window.

Features

  • Clean Architecture: core domain separated from infrastructure
  • Protocol-Based: easy to swap storage backends (Redis, custom)
  • Type-Safe: full type hints with Pydantic validation
  • Async First: built for asyncio applications
  • Fails open or closed: by default a Redis outage runs the action unreserved; on_storage_error="raise" refuses instead
  • In-flight Reservation: a retry that arrives while the original is still running waits for its result or gets a 409; the action runs once
  • Request Fingerprints: name the parameters that identify a request, and a key reused for a different one is refused instead of replayed
  • Observability: built-in metrics (hits, misses, collisions, latency)
  • Bulk Operations: efficient get_many, save_many, delete_many
  • Redis Cluster Compatible: non-transactional pipelines
  • Decorator Pattern: @async_idempotent for zero-boilerplate integration

When to use it, and when not

idempotency-kit fits an async Python 3.11+ service with Redis that wants idempotency at the use-case level, so the same guard covers an HTTP handler, a background job and a message consumer: retries that arrive while the first call is still running collapse into one execution, and a key reused for a different request is refused instead of replayed.

Something else is probably the better fit when:

  • You only need Idempotency-Key on a few HTTP endpoints. A middleware that caches the response by header is less to wire. There are several for ASGI frameworks, and django-idempotency-key for Django.
  • The write already has a natural key. If the effect can be an INSERT ... ON CONFLICT DO NOTHING on a unique constraint, the database is your idempotency layer and a second store adds nothing.
  • A repeat is unacceptable even when Redis is down. The reservation is a lease, and by default a store outage runs the action unreserved. For a charge or an email that nothing downstream deduplicates, set on_storage_error="raise", or keep the idempotency record in the same database transaction as the effect.
  • Your code is synchronous, or your store is not Redis. There is no sync API, and another store means implementing the repository protocol.

Installation

pip install idempotency-kit

# With Redis support (recommended)
pip install idempotency-kit[redis]

# With Dishka DI
pip install idempotency-kit[dishka,redis]

Requirements: Python 3.11+, Redis 6+

Quick start

from idempotency_kit import AsyncIdempotencyCoordinator, PydanticResultAdapter, async_idempotent

class CreateOrderUseCase:
    def __init__(self, uow: AsyncUnitOfWork, coordinator: AsyncIdempotencyCoordinator):
        self._uow = uow
        self.coordinator = coordinator

    @async_idempotent(
        operation="order.create",
        adapter=PydanticResultAdapter(OrderDTO),
    )
    async def execute(
        self,
        dto: CreateOrderDTO,
        idempotency_key: str | None = None,
    ) -> OrderDTO:
        """Create order - idempotency handled automatically."""
        async with self._uow.transaction() as tx:
            # Business logic only; the decorator handles the key
            order = await tx.orders.create(dto.items, dto.total)
            await tx.outbox.create(OrderCreatedEvent(order_id=order.id))
            
            return OrderDTO.from_entity(order)

Pass idempotency_key to downstream services for distributed idempotency:

# Orchestrate multiple services with same key
await identity_service.create_user(..., idempotency_key=idempotency_key)
await payment_service.charge(..., idempotency_key=idempotency_key)

How it works

1. Client provides key

POST /api/orders
Idempotency-Key: abc-123-def

2. Server checks cache

cached = await repo.get("order.create", "abc-123-def")
if cached:
    return cached.result  # Return immediately

3. Server executes (if not cached)

order = await create_order(dto)
await repo.save(record)  # Cache result for future requests
return order

4. Concurrent requests handled

If two requests arrive while the first is still executing:

  • First request: reserves the key, executes, writes the result over the reservation
  • Second request: finds the reservation, waits for the first result and returns it (or gets a 409 with in_flight="raise")

Both requests get the same result, and the business logic ran once.

Use cases

  • HTTP APIs: ensure POST/PUT requests are idempotent
  • Background Jobs: prevent duplicate processing on retries
  • Event consumers and queues: a redelivered message gets the stored result instead of a second run

Documentation

Full documentation

Development

make install          # uv sync --group dev
make check            # ruff + mypy
make test-unit        # unit tests (no Docker)
make test-integration # integration tests (Docker required)
make test             # all tests with coverage
make docs-serve       # local docs preview

See CONTRIBUTING.md for the full guide.

License

Apache 2.0

About

Idempotent request handling for async Python applications

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages