JPK większy niż 100 MB — limit siedzi w aplikacji, nie w bramce
12.08.2026
Pierwsze zderzenie wygląda zwykle tak: księgowość generuje JPK_KR_PD, plik ma kilkaset megabajtów,
a aplikacja Ministerstwa odmawia go przyjęcia. W dokumentacji stoi, że pliki większe niż 100 MB
należy „odpowiednio podzielić i przesłać każdą część osobno”. Brzmi jak instrukcja. W praktyce
jest to zdanie, które przerzuca problem na podatnika i nie mówi, jak go rozwiązać.
Ten tekst jest o tym, dlaczego ten limit w ogóle istnieje, dlaczego nie dotyczy bramki, i co trzeba zrobić inaczej, żeby wysłać plik dowolnej wielkości.
Skąd się biorą tak duże pliki
JPK_KR_PD nie jest kolejnym raportem — to cyfrowe odwzorowanie ksiąg rachunkowych za cały rok,
uzupełnione o znaczniki podatkowe, dane kontrahentów i uzgodnienie różnic bilansowo-podatkowych.
Struktura obejmuje w praktyce każdy zapis na koncie. U podatnika, który księguje kilkaset tysięcy
dokumentów rocznie, wynikowy XML potrafi mieć setki megabajtów, a u największych — kilka gigabajtów.
To nie jest patologia ani błąd generatora. Tak wygląda plik, który z definicji zawiera wszystko.
Obowiązek nie jest hipotetyczny i nie jest już przed nami. Za rok 2025 pierwsze JPK_KR_PD składają
podatnicy CIT i PIT prowadzący księgi rachunkowe, którzy są jednocześnie obowiązani przesyłać
JPK_V7M. Pozostali — rozliczający się kwartalnie przez JPK_V7K oraz zwolnieni z VAT — wchodzą
dopiero od 1 stycznia 2027. Pierwotny termin złożenia, 31 marca, rozporządzenie z 16 lutego 2026
przesunęło na koniec siódmego miesiąca po zakończeniu roku podatkowego, czyli 31 lipca 2026.
Czyli te pliki nie „powstaną kiedyś” — one właśnie poszły. Jeśli ktoś odbił się od limitu, zrobił to kilka tygodni temu, pod terminem.
Dlaczego „podziel plik” to nie jest odpowiedź
Rada z dokumentacji zakłada, że plik da się przeciąć. JPK nie da się przeciąć w sensie, w jakim dzieli się archiwum na części — to jeden dokument XML z nagłówkiem, okresem, podmiotem i sekcjami kontrolnymi liczonymi dla całości.
Trzeba tu być uczciwym: dla JPK_KR_PD Ministerstwo dopuszcza składanie plików częściowych
za dowolnie wybrane okresy, pod warunkiem zachowania ciągłości — bez luk i bez powtórzeń w każdym
węźle. Da się więc raportować w kawałkach, tylko że to już decyzja o tym, co raportujemy, a nie
o tym, jak to wysyłamy. Nagle trzeba uzgodnić z doradcą, jak pociąć okresy, jak numerować
dokumenty i czy podział niczego nie gubi. Problem techniczny zamienia się w merytoryczny wyłącznie
dlatego, że narzędzie do wysyłki miało limit.
Jest też struktura, w której ta furtka nie istnieje. JPK_ST_KR — środki trwałe — jest składana
za cały rok podatkowy i nie przewiduje podziału na okresy. Jeśli przekroczy 100 MB, nie ma czego
uzgadniać z doradcą: albo wysyłka idzie inną drogą niż darmowa aplikacja, albo nie idzie wcale.
Limit jest w kliencie, nie w protokole
Tu jest sedno. Bramka przyjmuje jeden dokument podzielony na wiele części — i robi to natywnie.
Protokół wysyłki wygląda w uproszczeniu tak:
- Plik jest pakowany do ZIP.
- Archiwum jest dzielone na części.
- Każda część jest szyfrowana kluczem AES-256 (CBC, dopełnienie PKCS#7).
- Powstaje dokument
InitUpload: metadane wysyłki, klucz AES zaszyfrowany kluczem publicznym Ministerstwa, skrót SHA-256 całego oryginalnego pliku, jego rozmiar — oraz lista części, każda z własną nazwą, rozmiarem i skrótem MD5 zaszyfrowanej zawartości. InitUploadjest podpisywany i wysyłany. W odpowiedzi wraca numer referencyjny i adresy, pod które należy wgrać poszczególne części.- Części lądują na wskazanych adresach, po czym wysyłka jest domykana.
Zwróćcie uwagę na punkt 4, bo tu najłatwiej o kosztowną pomyłkę: skróty są dwa i liczone różnymi
algorytmami. SHA-256 dotyczy całego oryginalnego XML-a przed pakowaniem i służy bramce do
sprawdzenia, że złożony z powrotem dokument jest tym, który zadeklarowaliście. MD5 dotyczy
każdej zaszyfrowanej części z osobna — ta sama wartość wraca potem w nagłówku Content-MD5
przy wysyłce części i po niej FinishUpload rozpoznaje, co zostało wgrane.
Kto policzy SHA-256 dla części, dostanie odrzucenie. Kto policzy MD5 z części przed zaszyfrowaniem — również.
Lista części jest elementem specyfikacji, nie obejściem. Protokół od początku zakłada, że jeden logiczny dokument przyjeżdża w kawałkach. Innymi słowy: 100 MB to ograniczenie konkretnej aplikacji klienckiej. Podatnik dostaje je w spadku razem z narzędziem, ale nie wynika ono z tego, co potrafi przyjąć urząd.
Kolejność kroków decyduje o wszystkim
Skoro protokół dopuszcza wiele części, cała trudność przenosi się do implementacji — i tam o powodzeniu decyduje jedna rzecz: w którym momencie dzielimy plik.
Naturalny odruch to spakować, zaszyfrować, a potem podzielić szyfrogram na kawałki. To nie jest
wolniejszy wariant tego samego — to wariant niepoprawny. Każda część musi być samodzielnym
kryptogramem AES-CBC z własnym dopełnieniem PKCS#7 (stąd nazwy w rodzaju .zip.001.aes). Szyfrogram
pocięty po fakcie nie da się odszyfrować po stronie bramki i wysyłka wraca z błędem. Przy okazji
wariant ten wymaga też trzymania całości w jednym kroku, więc przewraca się na dużym pliku — ale
nawet gdyby pamięci starczyło, i tak by nie zadziałał.
Właściwa kolejność jest odwrotna:
// 1. ZIP — strumieniowo, prosto na dysk
var spakowany = _filePreparer.CompressFile(plikWejsciowy);
// 2. Podział PRZED szyfrowaniem.
// Rozmiar podajemy w MB; 60 MB to dokładnie 62 914 560 bajtów,
// czyli maksimum dopuszczone dla pojedynczej części.
var czesci = fileDivider.Divide(spakowany, 60);
// 3. Szyfrowanie każdej części z osobna.
// EncryptFile zwraca ŚCIEŻKĘ do zaszyfrowanego pliku, nie jego zawartość —
// lista trzyma nazwy, nie bajty.
List<string> zaszyfrowane = czesci.Select(fileEncryptor.EncryptFile).ToList();
// 4. Skrót całości — SHA-256, liczony ze strumienia oryginału
var sha = _shaCalculator.CalculateSha(plikWejsciowy);
// 5. Skrót każdej części — MD5 z pliku JUŻ zaszyfrowanego
var podpisy = zaszyfrowane.Select(_fileSignatureFactory.Create).ToList();
Ta jedna zamiana miejscami — podział przed szyfrowaniem, nie po — sprawia, że żaden krok nigdy nie widzi więcej niż jedną część naraz. Rozmiar wejścia przestaje mieć znaczenie: dwa gigabajty to po prostu więcej części.
Reszta pipeline’u musi być spójna z tą zasadą. Kompresja idzie strumieniowo do pliku. Podział czyta wejście buforem i dopisuje do kolejnych plików wyjściowych. Skrót SHA-256 liczy się z otwartego strumienia, a nie z tablicy bajtów:
using var stream = File.OpenRead(plik); // tylko do odczytu, nie blokuje pliku na wyłączność
var hash = SHA256.HashData(stream); // .NET 5+; wcześniej: using var sha = SHA256.Create()
return Convert.ToBase64String(hash);
Gdzie to pęka w praktyce
Miejsca, w których implementacja przewraca się na dużym pliku, są zaskakująco powtarzalne:
File.ReadAllBytesna całym pliku. Najczęstsza przyczyna. W .NET pojedyncza tablica ma twardy limit tuż poniżej 2 GB, więc dla naprawdę dużego pliku to nie jest nawet kwestia ilości RAM-u — to wyjątek niezależny od tego, ile pamięci ma maszyna. Przy mniejszych plikach dostajecie wersję łagodniejszą: alokację w stercie dużych obiektów i proces, który puchnie do rozmiaru wejścia. Uwaga:ReadAllBytesbywa ukryty wewnątrz kroku szyfrowania — i tam jest nieszkodliwy dokładnie dopóty, dopóki operuje na jednej części, a nie na całości.- Wspólny IV dla identycznych części. Klucz AES i wektor inicjujący są generowane raz i te same
obowiązują dla wszystkich części. Konsekwencja jest nieoczywista: dwie identyczne części dają
identyczny kryptogram, a więc identyczny MD5 — a
InitUploadodrzuca zgłoszenie, w którym zadeklarowano co najmniej dwa pliki cząstkowe o takim samym skrócie. Przy plikach z długimi ciągami powtarzalnych danych to nie jest scenariusz czysto teoretyczny. - Rozmiar samego
InitUpload. Podpisane żądanie inicjujące ma własny limit rzędu 100 KB, a każda część dokłada do niego kolejny wpisFileSignature. Bardzo drobny podział bardzo dużego pliku potrafi więc uderzyć w limit z zupełnie innej strony niż pamięć. - Okno czasowe na wgranie części. Odpowiedź na
InitUploadniesie czas życia uprawnienia do zapisu (w przykładach specyfikacji rzędu 900 sekund). Wszystkie części muszą zmieścić się w tym oknie. Przy kilku gigabajtach i słabym łączu to bywa realniejsze ograniczenie niż cokolwiek po stronie kodu — i argument za tym, żeby wysyłkę uruchamiać z serwera, a nie z laptopa księgowej. - Proces 32-bitowy. Aplikacja zbudowana jako x86 przewróci się dużo wcześniej. Warto to sprawdzić, zanim zacznie się szukać błędu w logice.
- Trzymanie wszystkich części naraz. Podział rozwiązuje problem tylko wtedy, gdy części są przetwarzane po kolei. Zebranie ich zawartości do listy w pamięci odtwarza dokładnie ten problem, który podział miał usunąć — dlatego lista wyżej trzyma ścieżki, nie bajty.
- Sprzątanie. Plik 2 GB po spakowaniu, podzieleniu i zaszyfrowaniu zostawia na dysku pliki tymczasowe. Jeśli katalog roboczy nie jest czyszczony, dysk kończy się po kilku wysyłkach.
Nikt nie musi na to patrzeć
Skoro rozmiar przestaje być przeszkodą, zostaje pytanie, kto ma klikać. Odpowiedź brzmi: nikt, i nie jest to nic wyszukanego.
Cała ciężka część — kompresja, podział, szyfrowanie, liczenie skrótów — to operacje na plikach, które nie potrzebują interfejsu. W aplikacji okienkowej wystarczy zdjąć je z wątku UI, żeby okno nie zamarło na kilka minut. Ale ten sam kod równie dobrze działa jako usługa Windows, zadanie w harmonogramie albo endpoint HTTP: system księgowy odkłada plik w uzgodnionym katalogu lub wysyła go do API, a wysyłka dzieje się w tle i wraca numerem referencyjnym oraz UPO.
Wtedy „wysłaliśmy JPK” przestaje być czynnością, a staje się zdarzeniem w logu.
Jedyny punkt, który realnie wymaga człowieka, to podpis — o ile podpisem jest podpis kwalifikowany osoby fizycznej, bo karta kryptograficzna czeka na PIN.
Pieczęć zamiast człowieka
Nasuwa się kwalifikowana pieczęć elektroniczna: przypisana do organizacji, nie do osoby, z założenia służąca do automatycznego opatrywania dokumentów. Podpisana nią wysyłka nie potrzebuje już nikogo przy klawiaturze.
Zanim jednak zaprojektujecie wokół tego automat, dwa zastrzeżenia — i to techniczne wymieniam pierwsze, bo bywa pomijane.
Specyfikacja bramki JPK opisuje osobę fizyczną. Jako techniki uwierzytelnienia metadanych
wymienia podpis kwalifikowany, podpis zaufany i AuthData, a pieczęć jest w eIDAS osobną kategorią
od podpisu. Certyfikat od polskiego dostawcy ma zawierać PESEL lub NIP właściciela, bramka
weryfikuje umocowanie i ma osobne kody odrzucenia dla braku pełnomocnictwa oraz dla atrybutów
certyfikatu. To wszystko czyta się tak, jakby pieczęć nie miała prawa przejść.
Kuszące jest przeniesienie tu doświadczenia z KSeF, gdzie pieczęć działa, bo uwierzytelnia certyfikat podmiotowy. To jednak inna bramka i inny model — dla wysyłki JPK nie mam potwierdzenia, że kwalifikowana pieczęć firmowa przechodzi. Nie twierdzę też, że nie przechodzi; twierdzę, że tego nie wiem, a różnica między jednym a drugim to w tym przypadku cała architektura.
Jeśli bezobsługowość ma stać na pieczęci, sprawdźcie to najpierw na środowisku testowym, z włączoną walidacją podpisu kwalifikowanego. Jeden przebieg zamyka temat w obie strony. Lepiej dowiedzieć się w lipcu niż pod terminem.
Drugie zastrzeżenie nie jest techniczne: czy pieczęć jest dopuszczalna jako podpis konkretnej deklaracji, to pytanie do doradcy podatkowego, nie do programisty. Ja odpowiadam za to, żeby oprogramowanie poprawnie podpisało i wysłało tym, czym mu każecie — nie za to, czy wolno.
Warto przy tym pamiętać, że bezobsługowość nie stoi i nie upada na pieczęci. Podpis to kilka sekund raz na wysyłkę; cała reszta — te kilkadziesiąt minut mielenia dwóch gigabajtów — i tak dzieje się bez człowieka.
Co z tego wynika
Limit 100 MB jest własnością narzędzia, nie obowiązku. Bramka przyjmuje jeden dokument w wielu
częściach, bo tak została zaprojektowana, a implementacja, która dzieli plik przed szyfrowaniem
i wszędzie indziej pracuje na strumieniach, przestaje odbijać się od rozmiaru wejścia. Nie znaczy
to, że granic nie ma w ogóle — są, tylko leżą wyżej i gdzie indziej: w rozmiarze podpisanego
InitUpload i w oknie czasowym na wgranie wszystkich części.
To nie jest egzotyczna inżynieria — to kilka decyzji podjętych we właściwej kolejności. Gorzej, że koszt ich niepodjęcia płaci się później i w niewygodnym momencie: pod terminem, przy pliku, który powstaje raz w roku i akurat teraz nie chce wejść.
Jeśli mierzycie się z tym u siebie — czy to w SAP-ie, który nie unosi struktury, czy we własnym systemie — napiszcie. Powiem, czy to jest robota na tydzień, czy na kwartał, także wtedy, gdy odpowiedź brzmi „nie u mnie”.
Podłączam firmowe systemy do KSeF i JPK. Umów bezpłatną rozmowę, jeśli mierzysz się z podobnym problemem.