Skip to content

Query Limits and Constraints

The Danish Parliamentary OData API implements several important limits and constraints to ensure optimal performance and system stability. Understanding these limits is crucial for building robust applications that handle large datasets efficiently.

Overview

The API enforces a strict 100-record maximum per request while providing excellent performance characteristics. Large datasets must be accessed through pagination using $skip and $top parameters.

Maximum Records Per Request

Hard Limit: 100 Records

The API enforces a hard limit of 100 records per request regardless of the $top parameter value:

# These requests all return exactly 100 records
curl "https://oda.ft.dk/api/Sag?%24top=500"     # Returns: 100 records
curl "https://oda.ft.dk/api/Sag?%24top=1000"    # Returns: 100 records  
curl "https://oda.ft.dk/api/Sag?%24top=10000"   # Returns: 100 records

Effective $top Parameter Behavior

  • Values 1-100: Returns exact number requested
  • Values > 100: Capped at 100 records (no error thrown)
  • Missing $top: Returns 100 records (default)
# Working within limits
curl "https://oda.ft.dk/api/Sag?%24top=25"      # Returns: 25 records
curl "https://oda.ft.dk/api/Sag?%24top=100"     # Returns: 100 records
curl "https://oda.ft.dk/api/Sag?%24top=150"     # Returns: 100 records (capped)

Query Complexity Limitations

Expansion Limits

  • Maximum Expansion Depth: 2 levels
  • Multiple Expansions: Supported but impacts performance
  • Complex Expansions: Can significantly increase response size
# Single level expansion (fast)
curl "https://oda.ft.dk/api/Sag?%24expand=Sagskategori&%24top=10"

# Two-level expansion (slower, larger response)
curl "https://oda.ft.dk/api/Dokument?%24expand=DokumentAktør/Aktør&%24top=10"

# Deep expansion impact on response size
# Single entity: ~2KB ’ With expansion: ~15KB+

Filter Complexity

No specific limits detected for filter complexity, but best practices apply:

# Simple filters (recommended)
curl "https://oda.ft.dk/api/Sag?%24filter=typeid%20eq%201"

# Complex boolean logic (supported)
curl "https://oda.ft.dk/api/Sag?%24filter=typeid%20eq%201%20and%20statusid%20eq%202"

Performance Thresholds

Response Time Characteristics

Based on comprehensive testing, response times scale predictably:

Query Size Response Time Use Case
1-50 records 85-95ms Interactive queries
51-100 records 90-105ms Standard pagination
With expansions 108-200ms Related data queries
Large skip offsets 90-120ms Deep pagination

Performance Optimization Guidelines

# Optimal: Small batches with specific fields
curl "https://oda.ft.dk/api/Sag?%24top=50&%24select=id,titel,typeid"

# Good: Standard pagination
curl "https://oda.ft.dk/api/Sag?%24top=100&%24skip=200"

# Acceptable: Single expansion
curl "https://oda.ft.dk/api/Sag?%24top=25&%24expand=Sagskategori"

# Use carefully: Multiple expansions
curl "https://oda.ft.dk/api/Sag?%24top=10&%24expand=Sagskategori,SagAktør"

Rate Limiting Policy

No Rate Limits Detected

The API does not implement traditional rate limiting:

  • Concurrent Requests: 10+ simultaneous requests supported
  • Rapid Requests: 5 consecutive requests within seconds - all successful
  • No Throttling: No HTTP 429 (Too Many Requests) responses observed

Despite no enforced rate limits, implement responsible usage:

// Recommended: 100ms delay between requests
async function fetchWithDelay(url) {
    const response = await fetch(url);
    await new Promise(resolve => setTimeout(resolve, 100));
    return response;
}

// Batch processing with throttling
async function fetchAllPages(entityName, batchSize = 100) {
    const results = [];
    let skip = 0;
    let hasMore = true;

    while (hasMore) {
        const url = `https://oda.ft.dk/api/${entityName}?$top=${batchSize}&$skip=${skip}`;
        const response = await fetchWithDelay(url);
        const data = await response.json();

        results.push(...data.value);
        hasMore = data.value.length === batchSize;
        skip += batchSize;
    }

    return results;
}

Pagination Requirements

Mandatory for Large Datasets

All datasets exceeding 100 records require pagination:

# Get total count first
curl "https://oda.ft.dk/api/Sag?%24inlinecount=allpages&%24top=1" | jq '.["odata.count"]'
# Returns: 96538

# Calculate pages needed: 96538 ÷ 100 = 966 pages

Efficient Pagination Pattern

import requests
import time

def fetch_all_records(entity_name, batch_size=100):
    """Fetch all records from an entity with proper pagination."""
    base_url = f"https://oda.ft.dk/api/{entity_name}"
    all_records = []
    skip = 0

    while True:
        # Build URL with proper encoding
        params = {
            '$top': batch_size,
            '$skip': skip,
            '$inlinecount': 'allpages' if skip == 0 else None
        }

        # Remove None values
        params = {k: v for k, v in params.items() if v is not None}

        response = requests.get(base_url, params=params)
        data = response.json()

        # Add records to collection
        records = data.get('value', [])
        all_records.extend(records)

        # Check if we have more records
        if len(records) < batch_size:
            break

        skip += batch_size

        # Respectful delay
        time.sleep(0.1)

    return all_records

# Usage example
all_cases = fetch_all_records('Sag', batch_size=100)
print(f"Retrieved {len(all_cases)} total cases")

Large Dataset Access Strategies

For the largest entities in the API:

Entity Record Count Pages (100/page) Est. Time
Sag 96,538+ 966+ ~2-3 minutes
Aktør 18,139+ 182+ ~30 seconds
Dokument 500,000+ 5,000+ ~10-15 minutes
Stemme 2,000,000+ 20,000+ ~45-60 minutes

Timeout Thresholds

No Hard Timeouts Observed

  • Standard Queries: No timeout limits detected
  • Complex Queries: Large expansions complete successfully
  • Long-Running: Multi-thousand record retrievals complete

Client-Side Timeout Recommendations

// Recommended client timeout configuration
const fetchWithTimeout = async (url, timeoutMs = 30000) => {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

    try {
        const response = await fetch(url, {
            signal: controller.signal
        });
        clearTimeout(timeoutId);
        return response;
    } catch (error) {
        clearTimeout(timeoutId);
        throw error;
    }
};

Best Practices for Working Within Limits

1. Optimize Query Structure

#  Good: Specific fields only
curl "https://oda.ft.dk/api/Sag?%24select=id,titel&%24top=100"

# L Avoid: All fields with large expansions
curl "https://oda.ft.dk/api/Sag?%24expand=SagAktør,Sagskategori,SagDokument&%24top=100"

2. Implement Efficient Pagination

def paginate_efficiently(entity_name, filters=None):
    """Efficient pagination with built-in limits handling."""

    params = {
        '$top': 100,  # Always use maximum allowed
        '$skip': 0,
        '$inlinecount': 'allpages'
    }

    if filters:
        params['$filter'] = filters

    # First request to get total count
    response = requests.get(f"https://oda.ft.dk/api/{entity_name}", params=params)
    data = response.json()

    total_count = data.get('odata.count', 0)
    total_pages = (total_count + 99) // 100  # Ceiling division

    print(f"Total records: {total_count}, Pages: {total_pages}")

    all_records = data.get('value', [])

    # Fetch remaining pages
    for page in range(1, total_pages):
        params['$skip'] = page * 100
        params.pop('$inlinecount', None)  # Only needed for first request

        response = requests.get(f"https://oda.ft.dk/api/{entity_name}", params=params)
        page_data = response.json()
        all_records.extend(page_data.get('value', []))

        # Progress indicator
        if page % 50 == 0:
            print(f"Progress: {page}/{total_pages} pages ({len(all_records)} records)")

        time.sleep(0.1)  # Rate limiting

    return all_records

3. Handle Large Datasets Asynchronously

// Async generator for memory-efficient processing
async function* fetchAllRecordsPaginated(entityName, batchSize = 100) {
    let skip = 0;
    let hasMore = true;

    while (hasMore) {
        const url = `https://oda.ft.dk/api/${entityName}?$top=${batchSize}&$skip=${skip}`;

        try {
            const response = await fetch(url);
            const data = await response.json();

            const records = data.value || [];

            if (records.length > 0) {
                yield records; // Yield batch of records
                hasMore = records.length === batchSize;
                skip += batchSize;

                // Respectful delay
                await new Promise(resolve => setTimeout(resolve, 100));
            } else {
                hasMore = false;
            }
        } catch (error) {
            console.error(`Error fetching page at skip ${skip}:`, error);
            break;
        }
    }
}

// Usage: Process large datasets without memory issues
async function processAllCases() {
    for await (const batch of fetchAllRecordsPaginated('Sag')) {
        // Process each batch of up to 100 records
        batch.forEach(case => {
            // Process individual case
            console.log(`Processing case: ${case.id}`);
        });
    }
}

4. Monitor and Handle Errors

def robust_api_request(url, max_retries=3):
    """Make API request with error handling and retries."""

    for attempt in range(max_retries):
        try:
            response = requests.get(url, timeout=30)
            response.raise_for_status()
            return response.json()

        except requests.exceptions.Timeout:
            print(f"Timeout on attempt {attempt + 1}")
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # Exponential backoff

        except requests.exceptions.RequestException as e:
            print(f"Request failed on attempt {attempt + 1}: {e}")
            if attempt == max_retries - 1:
                raise
            time.sleep(1)

    raise Exception(f"Failed after {max_retries} attempts")

Issue: Getting 100 Records Instead of Requested Amount

Problem: Using $top=500 but receiving only 100 records

Solution: The 100-record limit is enforced. Use pagination:

# L This will only return 100 records
response = requests.get("https://oda.ft.dk/api/Sag?$top=500")

#  Use pagination for more records
def get_500_records():
    records = []
    for skip in [0, 100, 200, 300, 400]:
        params = {'$top': 100, '$skip': skip}
        response = requests.get("https://oda.ft.dk/api/Sag", params=params)
        records.extend(response.json()['value'])
    return records

Issue: Slow Performance with Large Expansions

Problem: Queries with multiple expansions taking several seconds

Solution: Optimize expansion strategy:

# L Slow: Multiple expansions in single request
curl "https://oda.ft.dk/api/Sag?%24expand=SagAktør,Sagskategori,SagDokument&%24top=50"

#  Fast: Separate requests for different expansions
curl "https://oda.ft.dk/api/Sag?%24expand=Sagskategori&%24top=100"
curl "https://oda.ft.dk/api/SagAktør?%24expand=Aktør&%24filter=sagid%20eq%201234"

Issue: Memory Issues with Large Dataset Processing

Problem: Application crashes when processing thousands of records

Solution: Use streaming/batch processing:

def process_large_dataset_streaming(entity_name, process_function):
    """Process large datasets without loading all into memory."""

    skip = 0
    batch_size = 100

    while True:
        # Fetch batch
        params = {'$top': batch_size, '$skip': skip}
        response = requests.get(f"https://oda.ft.dk/api/{entity_name}", params=params)
        data = response.json()

        records = data.get('value', [])
        if not records:
            break

        # Process batch immediately
        for record in records:
            process_function(record)

        # Clean up memory
        del records, data

        skip += batch_size
        time.sleep(0.1)

        # Progress update
        if skip % 1000 == 0:
            print(f"Processed {skip} records...")

Summary

The Danish Parliamentary OData API implements a straightforward but firm limit structure:

  • Hard limit: 100 records per request
  • No rate limiting: But implement client-side throttling
  • Excellent performance: 85-120ms response times
  • Pagination required: For datasets > 100 records
  • No timeout limits: Complex queries complete successfully

Success with large datasets requires proper pagination implementation and respectful request patterns. The API's consistent performance characteristics make it reliable for production applications processing parliamentary data at scale.