Overview
The Social Media Activity Feed API uses cursor-based pagination (also called keyset pagination) for the activity feed endpoint. This approach provides stable, high-performance pagination that scales better than traditional offset-based pagination.Cursor Pagination vs Offset Pagination
Why Cursor Pagination?
From the README: Stable performance at scale:- Offset pagination (
OFFSET n LIMIT m) forces the database to scan and skipnrows before returning results - As feeds grow, skipping thousands of rows becomes increasingly expensive
- Cursor pagination uses indexed
WHEREclauses that perform consistently regardless of page depth
- With offset pagination, new posts inserted while paginating can cause duplicates or skipped items
- Cursor pagination uses a deterministic “seek” predicate based on the last seen item
- Results remain consistent even when new posts are added during pagination
How It Works
Cursor pagination encodes the position in the result set using values from the last returned item:- Initial request: No cursor provided, returns first page
- Response includes cursor: Encodes the last item’s position (CreatedAt timestamp + PostID)
- Next request: Client passes cursor back to get next page
- Query uses cursor: Database seeks to items after the cursor position
Cursor Format
The cursor encodes two values from the last post in the current page:Why Two Values?
Using bothCreatedAt (timestamp) and PostID ensures:
- Uniqueness: Two posts might have the same timestamp
- Deterministic ordering: Breaking ties with PostID ensures consistent sort order
- Efficiency: CreatedAt is indexed for fast range queries
Implementation
Cursor Encoding/Decoding (feed.action.cs:59)
Feed Query Pattern (feed.action.cs:10)
Query Pattern Breakdown
The cursor predicate implements the “seek” pattern:Response Structure
Feed Response Format
Response Fields
- posts: Array of post objects (up to
limititems) - cursor: Opaque cursor string for fetching the next page (null if no more results)
- hasMore: Boolean indicating whether more results exist
Request Examples
First Page (No Cursor)
limit(optional, default=20): Number of posts to return per page
Subsequent Pages (With Cursor)
cursor: Opaque cursor string from previous responselimit(optional, default=20): Number of posts per page
Complete Pagination Example
Performance Characteristics
Index Requirements
For optimal performance, the Posts table has an index onInitiatorID (Post.cs:5):
- Filter posts by followed user IDs
- Seek to the cursor position using CreatedAt/PostID
- Sort results in the correct order
Query Complexity
- First page: O(log n) seek + O(limit) fetch
- Subsequent pages: O(log n) seek + O(limit) fetch
- No difference in performance between page 1 and page 1000
- First page: O(limit) fetch
- Page 1000: O(1000 * limit) scan + O(limit) fetch
Fetching limit + 1
The query fetches one extra item:HasMore detection:
- If
posts.Count > limit, more results exist - Remove the extra item before returning
- No need for a separate
COUNT(*)query
Error Handling
Invalid Cursor
If the cursor cannot be decoded:400 Bad Request
Missing Authentication
401 Unauthorized
Best Practices
- Treat cursors as opaque: Don’t parse or modify cursor strings client-side
- Cache cursors: Store cursor with fetched data to support “load more” functionality
- Default limit: Use reasonable page sizes (10-50 items) to balance latency and round-trips
- Handle null cursor: When
cursorisnull, you’ve reached the end of the feed - Check hasMore: Use this flag for UI state (hide “Load More” button when false)